Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
Game Development Patterns with Unreal Engine 5

You're reading from   Game Development Patterns with Unreal Engine 5 Build maintainable and scalable systems with C++ and Blueprint

Arrow left icon
Product type Paperback
Published in Jan 2024
Publisher Packt
ISBN-13 9781803243252
Length 254 pages
Edition 1st Edition
Languages
Arrow right icon
Authors (2):
Arrow left icon
Stuart Butler Stuart Butler
Author Profile Icon Stuart Butler
Stuart Butler
Tom Oliver Tom Oliver
Author Profile Icon Tom Oliver
Tom Oliver
Arrow right icon
View More author details
Toc

Table of Contents (16) Chapters Close

Preface 1. Part 1:Learning from Unreal Engine 5
2. Chapter 1: Understanding Unreal Engine 5 and its Layers FREE CHAPTER 3. Chapter 2: “Hello Patterns” 4. Chapter 3: UE5 Patterns in Action – Double Buffer, Flyweight, and Spatial Partitioning 5. Chapter 4: Premade Patterns in UE5 – Component, Update Method, and Behavior Tree 6. Part 2: Anonymous Modular Design
7. Chapter 5: Forgetting Tick 8. Chapter 6: Clean Communication – Interface and Event Observer Patterns 9. Chapter 7: A Perfectly Decoupled System 10. Part 3: Building on Top of Unreal
11. Chapter 8: Building Design Patterns – Singleton, Command, and State 12. Chapter 9: Structuring Code with Behavioral Patterns – Template, Subclass Sandbox, and Type Object 13. Chapter 10: Optimization through Patterns 14. Index 15. Other Books You May Enjoy

A world that ticks

In the last chapter, we covered the idea of the Update pattern. When using the Update pattern, we design with the philosophy that lots of different types of classes need to execute code on each frame. To achieve this, we will use an update function or, in the case of Unreal, a Tick Event. This pattern provides a fast way to make things happen, which is why it has a certain gravity, pulling many learning resources into leaning on it heavily. The volume of learning materials doesn’t make this a good idea, but it does allow systems to be propped up quickly, thus making learning from resources like this easy to understand. The problem lies in the seeming loss of understanding, over the cost of relying on this method. When we place any nodes or lines of code under the purview of tick, we need to remember it runs once per frame. On modern machines this equates to an average of 60 times a second, but it can vary depending on hardware, which is an important consideration for developers. For small games on modern hardware, the odd variable set or transform change may be fine; however, this is significantly more concerning if you consider the impact of a loop. This should make it clear that, as powerful as tick may seem, a web of problems lies beneath its surface.

To illustrate the problem, let’s look at the AGuardTower_CH5_1 actor in the Chapter 5 folder. We have followed best practice with the header file, using forward declared classes for member component definitions and the most limited property specifiers that we can. You will probably notice though that the only function, bar the constructor, is Tick. In this function, we carry out all our searchlight functionality. Stepping through the tick function on the searchlight, we have the following:

  • A call to the parent Tick
  • A sphere trace that gets reference to any actor that falls within the rough area of the light
  • A check to see whether the detected actor is of the player pawn type
  • Code branches to rotate the searchlight in every direction when the player has not been found

The GuardTower body file contains a tick function which looks like this:

GuardTower_CH5_1.cpp Tick function

void AGuardTower_CH5_1::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    FVector startLocation = _Arrow->GetComponentLocation();
    FVector endLocation = _Arrow->GetComponentLocation() +         (_Arrow->GetForwardVector() * _DetectionRange);
    FHitResult hit;
    UKismetSystemLibrary::SphereTraceSingle(GetWorld(),startLocation,
    endLocation,_DetectionRadius, UEngineTypes::
        ConvertToTraceType(ECC_Visibility), false, {},
            EDrawDebugTrace::ForOneFrame,hit, true);
    ACharacter* otherCasted = Cast<AEliteUnit>(hit.GetActor());
    _EnemySpotted = (otherCasted != nullptr);
    if (!_EnemySpotted)
    {
        if (_RotateForward)
        {
            _LightPivot->AddLocalRotation(FRotator(0.0, 0.2, 0.0));
            if (FMath::IsNearlyEqual                 (_LightPivot->GetRelativeRotation().Yaw, 40.f))
            {
                _RotateForward = false;
            }
        }
        else
        {
            _LightPivot->AddLocalRotation(FRotator(0.0, -0.2, 0.0));
            if (FMath::IsNearlyEqual                 (_LightPivot->GetRelativeRotation().Yaw, -40.f))
            {
                _RotateForward = true;
            }
        }
    }
}

This code does run, and you can see that if you drag an instance of BP_GuardTower into the level provided in the Chapter 5 folder. However, there are some issues, so let’s break down these problems in order:

  • With all the logic being on tick, we are performing some costly actions often. A sphere trace on its own is not too costly, but if we perform that sphere trace once per frame, per guard tower in a scene, the computational cost can add up quickly.
  • Quite a few getter functions are repeatedly used without caching. The world pointer could have been cached on BeginPlay, as that is unlikely to change, and multiple calls to the arrow component’s location could be done once. These are only minor improvements.
  • Casting is nowhere near as costly as it used to be in the early days of Unreal Engine. That said, when casting to a class, the target class must be loaded to ensure a match. This must be kept in mind when casting to a large class, as it can inflate the size of the process in RAM. Alternatives could include casting to an interface if you only need some functionality, checking tags if it is just a validity check, or, even better, trying to flip the communication on its head and remove the need to cast.
  • We then have a pattern that we will refer to as gated polling. On tick, check to see whether a block of code needs to run; if so, run it. This gated polling pattern is repeated a couple of times for different blocks of code. The alarm bells should now be ringing but maybe for the wrong reason. Yes, we have a repeated pattern of code, and we previously talked about avoiding repetition; that isn’t the real issue here. Any time we process something that may or may not need to happen, we guarantee a wasted check on some frames when it fails. We should consider how we can avoid the check and only process the code when it does need to happen.
  • Lastly, we have a compound problem with each branch of the gated poll. Yes, we are hardcoding values, both the rotation limit and the speed of rotation. Turning the hardcoded 0.2 and 40 into float member variables called _RotationSpeed and _RotationLimit respectively is a simple fix for the first part. Adding EditAnywhere to the UPROPERTY() block above each new variable will allow designers to not only balance the values in the actor but also create varied instances in their levels. This still leaves an issue with our code. Tick is not constant, yet we are using a constant value for rotation. This is where DeltaTime comes in. It is an argument of tick passed in as the time since the last frame rendered. Multiplying values by this will cause them to be applied evenly across 1 real-world second.

It should be pretty clear that there is a lot of room for improvement with this code, but how much room? Back in Chapter 3, we discussed Big O notation, but to get a higher resolution analysis, we need to calculate the T(n) or time efficiency of the algorithm. This should provide us with a baseline to prove that we can quantifiably do better.

The first sphere trace section contains roughly three assignments, six function calls, three arithmetic operations, and the internal time of a sphere trace. The cast section is two function calls, one test, and one assignment. In the if block, both branches are basically the same number of executions, so we will only count the top branch, giving us two tests, four function calls, and one assignment. Altogether, this makes roughly 23 + 1 * sphere trace executions per frame, with no overhead beyond a standard setup. Over a second at 60 frames per second (FPS), we would be running 1380 + 60 * sphere trace executions.

Important note

You can find the full working solutions for the following two sections under the _2 and _3 versions of the GuardTower_CH5 class, but try to follow along with this section, making changes to the _1 version to get the most out of the chapter.

Now that we’ve reviewed the problems with using Tick for the guard tower, let’s begin to look at how we can fix it, removing our reliance on Tick and writing better code.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image