A recreation of Fortnite's building, editing, inventory, and combat systems in Unreal Engine 5. Leverages C++ with GAS. Fully networked and server-authoritative. Uses AWS cloud backend for authentication and game session management.
This project recreates the classic grid-snapping building system for all of the 4 major structure types found in Fortnite: walls, ramps, floors, and pyramids. Each structure has unique placement logic (rules for where a building should be placed based on where the player is looking) and unique connection rules (rules for how one structure can connect to other structures).
When aiming to place a structure, the system displays a blue ghost preview of the structure at the targeted location. The ghost preview turns red if the placement is invalid, such as if its floating in the air.
Building is locally predicted, feeling responsive and instant even on clients with higher ping. Server authority is still fully maintained, allowing the server to rollback incorrect predictions.
Here is a side-by-side comparison of this project with Fortnite.
At the core of the build system is the Build gameplay ability. Upon activation, the gameplay ability spawns a targeting actor, which is standard practice in GAS for user previews and getting target data. The targeting actor uses a placement strategy to update its position based on factors like where the character is looking and the positions of nearby structures.
Each structure type has a different placement strategy, which allows for custom targeting behavior. For example, walls can be placed through other structures, such as ramps, while ramps cannot. These custom behaviors can be observed through playtesting in Fortnite.
When the player confirms the placement of a structure, it reports the targeted location back to the build ability. Since this information is on the local client, the build ability sends the targeting information to the corresponding instance of the ability on the server. The server then validates the request by ensuring that the requested location follows all the placement rules for the structure type and the location is snapped to the grid.
The client takes a slightly different path when doing building prediction. When a client sends off a request to place a structure, it places a locally predicted temporary actor of the requested building. This is tracked by the local player controller. I chose this since the player controller exists only on the owning client and the server, which is ideal since prediction doesn't affect simulated proxies. The client then deletes the temporary actor when the server responds to match the actual game state.
While implementing building prediction, I ran into a few challenges with character movement. See Character Movement tab for more on this.
In addition to building, this project also recreates Fortnite's famous structure editing mechanics. Players can reshape existing structures into similar structures, with different forms and properties. Each type of structure also has a unique method of editing. For example, walls use a 3x3 grid, ramps uses directions from dragging the mouse, floors uses a 2x2 grid, and pyramids uniquely has tile elevation when editing.
Edits also affect connection logic. For example, a ramp can connect to the top of a full-height wall, but not a half wall. As a consequence, editing a structure may disconnect it or its neighbors from nearby structures, causing them to collapse if not properly grounded.
Here is a side-by-side comparison showing this project with Fortnite, showcasing the editing system and structure collapse.
A somewhat less well-known Fortnite feature that this project also implements is the ability to edit your build previews. In other words, you can edit buildings before you even place them. After editing your build preview, all future builds of that structure type will use that edit until edited again. The editing behaves exactly the same as editing a placed structure.
Edited build previews use the connection rules for that specific edit type. This means that editing your build preview may make the structure unable to be placed in the same way as the full structure.
One main observation I made about Fortnite's build system is that it is fully tile-based. As such, I decided to make edits as bitfields, where each bit corresponds to a tile being selected or unselected. This makes edits especially easy to work with and replicate over the network.
These bitfields can then be used as keys to data assets to find relevant edit info efficiently. To make these bitfields easy to work with and designer-friendly, I created a custom property editor that allows inputting grid states visually, rather than just editing a raw integer.
Edit bitgrids are still efficiently represented as a single integer internally, but it now has an "array-like" front-end for easy editor usage. The behavior of the edit ability is driven largely by the contents of the data assets that use these edit bitgrids, making the system very data-driven.
Editing is implemented using an Edit gameplay ability paired with a custom targeting actor. The targeting actor is responsible for showing the edit tiles that the player interacts, displaying edit preview meshes, and interpreting input as changes to the edit grid.
Each structure has unique behavior with how they're edited. For example, walls cannot be rotated during editing, ramps use a dragging selection, and pyramid tiles change in height when selected. Each structure has a unique targeting actor to account for this, and the edit ability chooses the right one depending on the type of structure being edited. Each structure type has a unique targeting actor to handle all of the variation of each structure. However, they all inherit from a base targeting actor class to provide common functionality and to give the edit ability a shared interface to interact with.
When an edit is confirmed, the targeting actor's currently held edit is sent up to the server for validation and execution. The information is sent up to the server as custom target data, which includes edit bitfield and the structure's rotation along the Z-axis. This follows the same server-authoritative model as the Build ability.
The case of editing the build preview is a little more complicated, as it requires communication between the build ability and the edit ability. The build ability listens for the edit ability to start through gameplay tags, hiding its structure actor and removing input mapping to avoid interfering with the edit ability. The edit ability then spawns its targeting actor. In this case, when the edit is confirmed, the edit information goes to the build ability locally rather than the server.
Something that's equally important as placing structures is destroying them. In Fortnite, structures must be connected to ground (a "grounded" structure) to be valid. Free-floating structures are not allowed and get destroyed.
The approach I took is to consider connected structures as nodes in a graph. Whenever a structure is deleted, its neighbors are alerted, prompting each neighbor to check if its grounded or not. The ground check is done via breadth first search through the graph looking for ground. If it's not grounded (i.e. BFS exhausts the graph), it too is destroyed, further alerting its neighbors.
The most difficult part of all this was making it efficient enough to run seamlessly when destroying thousands of structures. I first noticed that destroying large structures resulted in considerable hitches in the game's performance. I used Unreal Insights to investigate, find the root causes, and implement optimizations. All screenshots of Unreal Insights will be captured during the destruction of the same tower of ~5000 actors.
When a structure is broken, the structures it was directly attached to may or may not be apart of the same overall building, but it is hard to say which one is the case. (in graph theory terms, we can't easily tell if the deleted structure was a cut vertex). As such, we need to check each neighbor separately to see if it is grounded or not. However, in the majority of cases where the neighbors are still connected, this results in a lot of repeated calculations.
To solve this, I cache the groundedness of a structure after I calculate it. This means each node has to be traversed through only once. Follow up traversals that visit the node, such as those from other neighbors of the deleted structure, simply check the cache and early return with that result.
bool UStructureGroundingComponent::IsGrounded()
{
// Structure is grounded by definition if directly colliding with terrain/world geometry
if (bIsAttachedToGround)
{
return true;
}
if (IsGroundCacheValid())
{
return bIsGroundedCached;
}
TSet SeenStructures{};
TQueue Queue{};
Queue.Enqueue(this);
while (!Queue.IsEmpty())
{
UStructureGroundingComponent* CurStructure = *Queue.Peek();
Queue.Pop();
const TSet& CurrentNeighbors = CurStructure->GetNeighbors();
for (const auto Neighbor : CurrentNeighbors)
{
if (SeenStructures.Contains(Neighbor))
{
continue;
}
SeenStructures.Add(Neighbor);
if (Neighbor->bIsGroundingStructure)
{
SetCacheOnStructures(SeenStructures, true);
return true;
}
if (Neighbor->IsGroundCacheValid())
{
bool bIsGrounded = Neighbor->GetGroundCache();
// Don't set ground cache if not grounded, since we're already initially predicting they aren't grounded
if (bIsGrounded)
{
SetCacheOnStructures(SeenStructures, true);
}
return bIsGrounded;
}
// Initially predict that structures won't be grounded
// If we are grounded, there will probably be less structure to cache
Neighbor->SetGroundCache(false);
Queue.Enqueue(Neighbor);
}
}
return false;
}
One of the biggest time sinks in the traversal was getting a list of structures adjacent to any given structure. This is a crucial step to any graph traversal algorithm and one that should be quick. I initially was using GetOverlappingActors() to achieve this functionality, which was a very expensive function to call so frequently.
Instead, I only call GetOverlappingActors() once when the structure is first placed to cache the neighbors list. After that, I manage the list manually by adding/deleting neighbors from the list as they are placed and destroyed.
The result is near instant retrieval of a structure's neighbors. Instead of iterating over every component and doing physics calculations (as GetOverlappingActors() does), retrieving neighbors is now just simply binding a reference to an already made list.
The next biggest slowdown was the actual actor deletion itself. This came after the actual graph traversal, but was still an important part of the system. Large buildings resulted in upwards of hundreds of actors being destroyed in a single tick, which is horribly inefficient.
To combat this, I implemented a destruction queue. After the graph traversal, instead of destroying all the actors immediately, they are simply disabled by turning off rendering and collisions. This is much faster than full actor destruction. Then, all these actors are then added to a queue. A few actors from these queued are destroyed every frame to even spreadly out the work over several frames.
The final issue I noticed wasn't with local CPU performance, but with network bandwidth. I initially had the collapse simulation happen only server-side, expecting the deletion of actors to be replicated to clients to show the effects of the collapse. However, due to the sheer amount of actors, this quickly became too much to replicate in a frame. Replication slowed down and eventually halted entirely from the overwhelming amount of traffic.
The solution to this was to make structure collapse locally predicted. Now, the server only alerts clients (just a single RPC!) of key actions that could possibly cause a structure to become ungrounded, like structure deletions and edits. The client then predicts the rest of the chain reaction with the same set of rules as the server to end up with the exact same result.
The final result is a structural collapse system that is fast and lean. While this could be even further optimized to drive down the current 3.3ms cost, I considered this to be a good enough stopping point at this time. The building destroyed in these tests is considerably larger than what's feasible in actual gameplay. Buildings being destroyed in gameplay should be drastically smaller and require a lot less compute time.
One of the many aspects of online service games like that have made me curious is scalability. At any given moment, there are thousands of game servers hosting thousands of players simultaneously, with more readily available based on activity. Each player has its own data like usernames, unlockables, and purchases. To learn more about how cloud architecture is used in games, I took a dive into Amazon Web Services - namely GameLift, API Gateway, Cognito, and Lambda.
Here is the client signing up, authenticating, and joining a server hosted on an EC2 fleet:
Using a version of Unreal Engine built from source and the AWS SDK, I packaged and uploaded a GameLift-enabled server build of this project. Players could then use in-game UI to create an account and login to the game. To do this, I sent API requests using Unreal's HTTP system to API Gateway, which was in turn hooked into custom AWS Lambda functions with Cognito functionality. Once logged in, users could make authenticated API requests to search for and join a game session server. The game server uses the AWS SDK to accept player sessions, which AWS uses to enforce player count constraints.
To make it easy to use HTTP requests, I created a versatile wrapper function and a corresponding Blueprint node to handle sending and receiving HTTP requests. API endpoints are defined flexibly through data assets. The request body is defined through a string map and the response content is automatically parsed into a typesafe struct.
While not implemented yet, this project's current AWS setup makes it easy to add extra functionality, which I look forward to doing in the future. For example, I can associate a database with Cognito to store/access player-specific data, enabling features like unlockables and stat tracking. I can also create other cloud services for functionality not specific to any one game server, like matchmaking and party systems.
Server authoritative, bandwidth optimizations, prediction
A core part of Fortnite's gameplay loop is the inventory system. I wanted to create a similar system with a focus on robustness, extensibility, and with easy-to-use designer workflows.
First, I carefully designed the system on paper before I started implementing. I thought about the different ways in which items would need to be used, how items would be acquired, and how responsibility for the system should be balanced between the server and clients. I also considered common behaviors and characteristics with items and considered ways to elegantly support them. I created this graph to organize my thoughts in the technical design process.
In this system, an item has two components: an actor and a data asset. The actor represents the specific instance of the item and holds instance specific data, like number of uses left or an ammo count. The data asset represents the type of item and describes general behavior that all instances of that item should have, like stacking behaviors, rarity, and how it is displayed in UI. For example, different instances of a large shield potion each have a unique actor, but they share a common Shield Potion data asset.
The item actor itself is primarily not responsible for behavior. Items are associated with gameplay abilities that are granted when equipped and removed when unequipped. These gameplay abilities are what actually perform the functionality of items. The player's inputs sends out events associated with GameplayTags that item abilities may trigger off of. For example, a healing item may grant a Heal GameplayAbility that is automatically triggered when the player sends an Input.Fire gameplay event.
This project uses a custom character movement component (CMC) to handle custom movement logic.
While the Gameplay Animation sample (which this project uses) implements sprinting by default, it isn't very robust. Testing the default sprinting behavior in less than perfect network conditions results in extensive rubberbanding and server correction. As such, I reimplemented sprinting to be more network safe. I also while also implemented stamina with a stamina regeneration lockout delay after sprinting.
While not difficult mechanics on their own, making these systems work well with CMC's prediction and simulation system took customizing many other parts of the CMC to get right (saved moves, move data, move data containers, etc). These systems have been tested in non-ideal network conditions to ensure full movement safety and accurate prediction.
One of the biggest challenges I faced when implementing build prediction was the local player being able to walk on the locally predicted structure actor. Conceptually, being able to predict moving onto a predicted structure makes sense. In most cases, the request to place the structure will arrive at the server around the same time as the request to move the player onto the structure, so the server should be able to simulate the exact same movement as the client. However, this wasn't the case in practice.
After much debugging and looking through source code, I isolated the problem to CMC's base calculations. By default, the client CMC tells the server what the player is standing on (it's "base"). However, if the player is standing on a client-only structure, the server will not be able to validate the client's reported base. This causes immense desync as the server moves the player differently due to the failed base calculations.
After trying many different solutions and approaches, the best one I came up with was to simply not allow non-replicated actors to be set as bases. In this case, both the client and the server are able to agree that the player's base is null. The tradeoff is that the player won't move with predicted structures that are moving, but this is not an issue for this project, as placed structures are static anyway.
With this change implemented, movement was smooth, even on actors that weren't even on the server yet!
This project implements server-side rewind hit detection to compensate for lag when using hitscan weapons. This is sometimes referred to as server-side rewind or Favor the Shooter in modern shooter games.
On the server, position data of every player's hitbox is stored along with a timestamp. When players request to shoot a weapon, they pass along timestamp data of the local time they shot the weapon. The server can use the player's request timestamp to recreate the hitbox positions at the time of the client's shot.
This project wraps this functionality this in a scoped window, called FLagCompensationWindow. Abilities that want to use rewind lag compensation only need to make a Window in its scope. The window automatically handles restoring to present state when it leaves scope. This design abstracts away the rewinding and unrewinding, providing flexibility with what can be used with lag compensation while still being easy to use.
Here is a basic example of this system in action:
void UFireWeaponAbility::ServerFire(const FWeaponTargetData& TargetData) const
{
FVector Start = TargetData.Start;
FVector End = TargetData.End;
FHitResult Hit;
{ // Lag compensation starts at beginning of scope
FLagCompensatedWindow Window{TargetData.RelevantTargets, TargetData.Timestamp};
// This can be any hit detection logic
GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility);
} // Lag compensation ends at end of scope
if(Hit.bBlockingHit)
{
// Apply damage, gameplay effects, etc.
}
}