This is a version of the document I used at Question as its Tech Lead.
Permission to use this on my portfolio was granted by my gracious former business partners.
First of all, you will never get in trouble for not remembering what's on this page. Expecting somebody to remember everything here is madness.
What we do, instead, is use this as permission to tell somebody to change something during code reviews or post-checkin audits.
New employees will be having all of their checkins code-reviewed and audited when they first join our team. The idea is for new employees to internalize what's on this page through osmosis with other members of the team. We all come in from other studios and internet forums with our own set of conventions. The purpose of having studio coding conventions is to make sure we can all work as a team through shared practices. Code audits and reviews will ramp down over time as the need for convention and architecture mentoring ramps down.
The purpose of sharing common nomenclature is to serve the team, not one programmer.
Every convention on this page solves a particular problem for the "programmer-got-hit-by-a-truck" scenario.
Having to remember a book of rules sucks. The less conventions we have, the better. We don't want a religious war... we only need just enough of these to allow any programmer to work in another programmer's code with the lowest possible mental friction.
Whatever you do, DO NOT BREAK THESE CONVENTIONS WITHOUT A PROPER DISCUSSION, FIRST. They exist for a reason, and changing them defeats the purpose of having conventions.
Internal studio conventions take precedence over samples from the internet. Your code needs to look like everyone else's code in order to maintain maximum readability between programmers. Specific examples of conventions from internet code samples that would be out of place in our codebase include:
In the cases above, those keywords are either redundant or conflicting.
We do not want to have to write a bullet point on this page for everything on the internet that you shouldn't do... So this section establishes the standard employment practice of "When in Rome, code like the Romans do."
It doesn't have to be a perfect match
Some things we don't care about:
The threshold for acceptable divergence scales with its pragmatic value. If your pragmatism is not obvious, then radiation will build up for your check-ins. When the radiation meter gets high enough, A coworker will eventually approach you and ask why you are checking in code that looks different from the other programmers.
Unacceptable answers include:
Just because you see something on the internet doesn't mean you should import this convention into the studio, even if the code sample is from Epic. Our coding standards convey more information than Epic's standards, and every change is done with the consensus of existing team members.
A good convention to bring into the studio is one that you can defend with pragmatic values.
Because our code may be applied to multiple projects under multiple funders. Keep this in mind. Comments and variable/function names may affect their perception of us.
Because we don't want designers to have to do unit conversions across different systems
Because we need to know the difference between code we can trust to be relevant to our game vs. code written by Epic that may not apply to our stuff.
This applies to class names as any name that shows up in Blueprint or Editor categories.
The intent here is to let humans recognize and/or quickly find functionality that we write versus the stock functionality that came from Epic... because not everything that Epic exposes is something that we want to use for this project.
Secondary intent is for make it easier to communicate gameside vs. engineside issues from a callstack.
Yes, we know that Epic relaxed this requirement in 4.8, but we want to hold onto this standard to minimize dependencies between translation units.
The idea here is that we want to make sure incremental linking is as efficiently supported as possibly by reducing dependencies between separate UClasses as much as possible.
Because this lets coders know upon first sight whether a variable is "class-scoped" (affects the entire class) versus "function-scoped" (affects just what's in that function).
In the case of Editor properties, we stick with "The Unreal Way", which is Capitalized with CamelCase.
It would be really nice if Unreal did what you would expect in naked C++, but it doesn't. Because of that, we must do these things...
Because naked pointers can get garbage collected when nothing references them, and that can lead to very hard to find memory corruption crashes with inconsistent callstacks.
BAD: Caused memory corruption crash
class UQtnBodyAnimGraphTP* m_charAnimInstanceTP;
GOOD: Fixed that memory corruption crash
UPROPERTY(Transient)
class UQtnBodyAnimGraphTP* m_charAnimInstanceTP;
for anything that is not meant to be temporary.
... because Unreal serialization may save that property's value and stomp whatever you do locally when the object deserializes.
See here for details: https://forums.unrealengine.com/showthread.php?54789-Where-to-use-a-Transient-variable
for member POINTER variables of UClasses with serializeable types that are not meant to be shared across all instances of that class.
... because Unreal will normally use the address of the class default object for that member variable if you do not do this. It is also a good practice to use the "DefaultToInstanced" class specifier for the classes you write to ensure that the pointer won't be shared with other instances. Failing to do this may lead to a bug that only manifests when you have more then one instance of your class.
Unreal does not generate default constructors for USTRUCTs. This means that when the structs are allocated on the stack, fields with primitive types will have undefined values. This matches the default C++ behavior, but since Unreal does zero-initialize structs in most other cases, it is easy to overlook initialization for USTRUCTs that are stack-allocated.
In addition to the normal unpredictable behavior of uninitialized member variables, if the struct has UPROPERTY pointers and is copied into a UPROPERTY field or collection, it can cause GC crashes.
When in doubt, just make sure every primitive-typed member of a USTRUCT has an initial value.
Replicated variables are usually what you want to use 80% of the time. Multicast is a thing you do when you are absolutely sure it won't matter if a new player joins a session in progress.
It gets reassigned all over the place and will sometimes point to an invalid world. Epic tells you not to use it.
See here for more info: https://answers.unrealengine.com/questions/394333/ue4-c-playercontrollerlist-in-gworld-is-empty.html#answer549469
The alternative is to do the following:
Note that is is not safe to use 'GEngine→GetWorldFromContextObject' as it is expensive and may also result in stack overflow.
So that Level Designers will be able to quickly filter by functionality by typing a predictable pattern for the thing they are looking for.
If the function has no side effects and can be trusted as a pure accessor, present this to designers as a BlueprintPure function so that they don't need to worry about input/output execution pins
Because BPPure functions get called for every pin. Explanation here: https://celdevs.com/2021/09/14/unreal-engine-and-the-hidden-pitfalls-of-blueprints/
Use BlueprintCallable if the function is expensive. Const causes it to render in BP as a BPPure function
Because documenting on Confluence doesn't always work for all cases and we don't want normal designer workflow to be to hunt on Confluence for every little thing when a tooltip will suffice.
UFUNCTION(BlueprintPure, Category = "ssgUtilities|Physics", meta = (WorldContext = "WorldContextObject", CallableWithoutWorldContext, tooltip = "Digs into the right place to expose this acceleration value in cm/s^2"))
static FVector GetWorldGravity(UObject const* const WorldContextObject);
Be sure to internalize rules for RPCs here: https://docs.unrealengine.com/latest/INT/Gameplay/Networking/Actors/RPCs/
It helps when you can know something about a function's intent from its name without having to jump inside it to understand its use pattern.
If you have a function that does "Foo" which results in network RPC calls, then here are the RPC functions to go with that.
In cases where we want instant response and trust the client (such as Jump), the Authoritative owner of that Actor might have LocalXXX call DoXXX if (ROLE_Authority == Role) in addition to calling ServerXXX, and then call DoXXX inside ClientXXX only if (ROLE_Authority != Role) so that the owner's representative on a remote machine will also jump.
If you are confident in RPC rules, you can often skip making LocalFoo and ServerFoo if you just want to call MulticastFoo from the host machine.
This reminds designers that this event happens on all clients and not just the server.
Designers don't get to see the C++ code, so it benefits them to get this friendly reminder that a version of this event happens on each machine and not just the local or host machine. Knowing whether an event is happening on the server or client can tell them whether to do a thing authoritatively or just show a local presentation.
This suffix is meant to parallel the meaning of Epic's BlueprintAuthorityOnly UFUNCTION modifier.
Sometimes, you want to call a function that should only be called on the Server, but you do not want the overhead of RPC function marshalling and callstack obfuscation that comes with UFUNCTION(Server). Nor do you want the chance that multiple clients might call this server function and cause redundant effects (such as stamina drain/regen being multiplied by number of clients). In this case, our convention is to add an "_Authority" suffix after the function name to indicate that it is meant to only be called on the host: "Foo_Authority()". It will be up to the caller to make sure this function never gets called on a client.
The reason this is mandatory is because we can easily lose time debugging stuff caused by the use of functions that were only meant to be called on the host machine, such as 'APlayerController::IsPlayerMuted'.
These are functions that only get called on the 1 local machine in the multiverse. Somewhat equivalent to Client RPC functions.
Exposed Blueprint events will be a common pattern, and so the purpose of this naming convention is to make it easy for the Level Designer to expect names within this pattern
Sometimes, we need to leave notes for our future selves or other programmers. Here is what we care about for that note:
//SSG_ENGINE_MOD [Kain: June 15th, 2026] Making this function virtual so that we can override it in gameside code...
virtual SomeEngineFunctionThatWasPreviouslyNonVirtual() const;
//...SSG_ENGINE_MOD
(SSG for ScreenSplitGames. Replace with your own acronym within your studio)
We want to mod the engine as little as possible in order to make future merges less error-prone. Use inheritance instead of engine modding, if you can. Sometimes this means a minor engine mod to make a base class function virtual.
Keep in mind that every engine mod has a nonzero chance of getting stomped by human error during the next Epic engine integration. This is the main reason to inherit and place things in gameside code instead of engine code whenever possible.
When you do mod the engine, it is absolutely critical that you note the mod with this tag so that the human merging future versions of the engine will know to preserve that custom functionality. This human will need to read your mod and understand what it means in order to make the best decisions during the merge process.
Unlike most of the conventions in this doc, conventions in this section do not have a pragmatic defense other than "it annoys the majority of engineers, here".
Make sure your IDE settings treat indents as tabs and not multiple spaces. This makes code uniformly readable to every team member without dealing with religious wars over whether an indent should be 3 spaces or 4 spaces.
Visual Studio usually standardizes bracing and indentation to typical standards, known as “ Allman Bracing ” and “indent after every brace to show that you are one level in something”.
Somewhat related to bracing style, one of the arguments for the use of K&R bracing is for code compactness that lets you fit more lines of code on the screen.
If you find yourself staring at a very large section of scope that does not fit on your screen, then consider these various techniques to make your code more readable in ways that are more effective than just using a more compact bracing style:
BAD:if (outerSomething)
{
if (innerSomething)
{
if (wut)
{
<very large block of code>
}
}
}
GOOD:if (!outerSomething)
{
return;
}
if (!innerSomething)
{
return;
}
if (!wut)
{
return;
}
<very large block of code>
Because it thwarts Rider and Visual Assist's h/cpp flipping feature
Control statements are statements that control how the code inside its block will execute, such as if, for, while, do, and foreach. Even if you have one line inside the if statement, you will want to use braces. The reason for this is to minimize the chance of human error and maximize readability by keeping syntax and formatting consistent.
BAD:if (something)
doSomething();
doAnotherSomething();
GOOD:if (something)
{
doSomething();
}
doAnotherSomething();
Because we value readability and want absolutely zero chance of having to ever investigate some bug coming from mismatched parameter expectations.
Sometimes, you know the exact name of the function, but you don't know where it lives.
Sometimes, you just need to find all uses of a function throughout the project.
Naming a function Init(), Update(), or Tick() means that a project-wide search will return all functions with that name.
Giving your Init/Update/Tick function a suffix (InitBodyState(), UpdateIntentions(), TickBrain()) makes it easier to teleport yourself to that function within the IDE.
Known as “The Yoda Conditional”. Hoomans can make mistakes by accidentally using ‘=ʼ instead of ‘==ʼ.
That gets combined with the fact that C++ lets programmers make both, assignments and comparisons, inside an if statement.
This will compile:
if( object = nullptr )
This will NOT compile: … because the compiler will complain that nullptr is a constant that you canʼt assign to.
if( nullptr = object)
Naming classes and their files like
BodyStateIdle, BodyStateHitReact, etc…
instead of
IdleBodyState, HitReactBodyState, etc…
Lets auto-complete present you classes in alphabetical order, which is convenient if you don't remember the exact name of the class.
This is now debatable amongst certain circles who believe that hardware has found ways to do this faster, but typically, the operation of taking the square root of something was significant enough to show up on profilers.
This is why you will often see code look like this:bool bCloseEnough = (headPosition - tailPosition).sqrMagnitude < (radius*radius);
instead of this:bool bCloseEnough = (headPosition - tailPosition).magnitude < radius;
… because .magnitude has to calculate the square root of that hypotenuse (x^2 + y^2 + z^2) in order to get the length of that vector.
It is a generally known fact that division takes more clock cycles of a CPU than multiplication. That is why we try to multiply by 0.5f instead of dividing by 2.0f whenever possible.
When in doubt, use const as much as possible wherever it makes sense, such as making accessor functions const because we do not want accessor functions with side effects.
This applies specifically to things that never get exposed to Blueprint. In Blueprint, you have no choice but to make output param non-const references.
OKAYvoid MyGetterFunction(MyClassType& outputParameter);
PREFERREDvoid MyGetterFunction(MyClassType* pOutputParameter);
... because when you call that function, it looks like...
RESULT OF OKAY FROM THE CALLER SIDEMyGetterFunction(outputParameter);
RESULT OF PREFERRED FROM THE CALLER SIDEMyGetterFunction(&outputParameter);
... in the PREFERRED case, you can immediately spot that outputParameter is going to be changed inside the function while the OKAY case is visually identical to calling a function with this signaturevoid MyGetterFunction(MyClassType const& outputParameter);
orvoid MyGetterFunction(MyClassType outputParameter);
Basically, passing in an address instead of reference is a self-commenting means of communicating a parameter as intended for output.
Because it makes a little icon show up in Blueprint that tells the Level Designer that this node will only execute on the host machine.
Useful for Server, Multicast, and BlueprintCallable functions that you don't want to be public.
Because it becomes distracting noise to other programmers.
We have source control to maintain file history if we ever need it.
If you feel that the old code will remain a relevant reference for the future, you can either write the takeaway from that old code as a comment or leave a comment with either a JIRA and/or the last approximate changelist on the file for easy retrieval if we ever need to refer to it.
Because extra #includes can bloat our compile time and cause unnecessary dependencies. Rider and maybe later versions of Visual Studio will catch unused #includes and repeats, but if not, you can get into the habit of alphabetizing your #includes at the top of your cpp to at least make it easier to catch those repeats.
Static variables can be very convenient because their values persist as long as the app is running; so you can trust them to never change unless you change it.
Not all static variables are bad. Global singletons for managers might be okay, especially if there is only one global singleton. Graphics and hardware-level statics might also be okay if those can't be changed by user actions during the lifetime of the exe. We use static variables for the memory pool that manages and recycles transition request objects for each FSMState class because those objects surviving across level transitions or in a multiplayer PIE session won't affect the recycling functionality of those managed object pools.
Static variables that change as a result of gameplay are the ones you need to watch out for. Not changing gameplay-related static variables when level transitions occur can lead to subtle hard-to-find bugs that only occur when the transition happened within a certain window of gameplay. These unplanned level transitions can occur either because of user input or because the profile on Xbox One changed, and the current user is no longer active (MS Cert requirement).
A general rule of thumb is to avoid the use of static variables related to user actions. If you are using a static bool that gets set to true in one place and false in another place, then that creates an AVOIDABLE risk that will need to be managed as complexity increases. An alternative is to make that variable a local member of some global manager class that has the proper centralized hooks for level transitions and changes in gameplay state.
Warning about Static Variables and PIE: Playing multiple clients from the editor will share static variables!
This might be okay for resource pools, but definitely not okay for anything that needs to stay in its own client world and not mix with other client worlds. If you need to maintain a variable that is global to a single client and it has no obvious instanced owner to consider its home (such as some list to register in BeginPlay and unregister at EndPlay, then consider "AQtnGameState" as the place to hang that variable. There are exactly one of these per client.
Primer: Replication Tactics for Designers
Hacker tools exist that can overwrite memory addresses and spoof network packets in order to do things such as cast heal spells with negative values that can kill an ally.
For this reason:
... Unless you think the risk is worth it.
There have been cases where waiting for the round trip to/from the host machine results in a bad player experience. Movement is an example; we like that movement feels lag free on client machines reguardless of ping time. Unreal basically lets the client move and then lets the server police that move after the fact. There may be other situations like movement that we'd want to apply the same mindset for.
This means that all of the RPC events you triggered in the past will not retrigger in the visitor's simulation upon joining.
For this reason: