Search
  • Jesse Humphry

Nativizing the Timer Component

One of the biggest initial hurdles for me as a developer was taking Blueprint-scripted components I'd developed and nativizing them in C++ to improve speed. I knew this would invariably be the next step in my career, as Blueprint is becoming more accessible (and unfortunately less valuable to clients). C++, however, still remains difficult despite how much easier the Unreal API makes the use of the language. I decided to start out by nativizing a fully-BP-scripted component, my TimerBehavior component (you can read about that here) Note: The original system used an interface to communicate first with the actor holding the timer component, and then with the timer.via its owner. I'll be cleaning up where we use the interface and explaining why I've made the decisions I did. Because of the use of the Blueprint Interface, I'll need to nativize this as well in order to make that interface accessible to other C++ classes as well as to Blueprints that may wish to utilize the interface. So let's get started.


Creating the Interface


Luckily, Unreal makes this part pretty easy; we can create a C++ class that inherits from Unreal's base Interface class. Incidentally, this will turn our project into a C++ project (it was previously BP-only, and I needed to do this soon anyway).


Next, we need to determine what functions we need to bring from the original interface. It's been months since I've looked at this project (as I've been bust with contract work), so I can already see that this isn't exactly a 'clean interface'.


The timer functions are included with a couple other interaction functions in BPI_Interaction. This may be convenient, but it certainly isn't clean. For now, we'll create an interface just for the timer functions.


Note: In the future, I'd rather create a child of AActor called AInteractiveActor that would contain virtual functions for all interactive actors; the interface isn't the best solution long-term.


In addition to this, we need to determine which interface calls aren't necessary. Let's jump into the Timer component itself and break down what we do and don't need.


BPI Functions in Timer


The first thing I noticed was that the timer component implemented an interface event TimerCheckIn. This doesn't make sense; we know that we're going to be talking to a component of type TimerBehavior every time we make a call like this.


The reason it was done this way is because I wasn't passing a direct object reference for the timer's component class, instead passing in the base ActorComponent object reference.


We can exclude this moving forward and replace it with a function for check-in built inside of the timer component. I also noticed a BP "guard clause" that just checked a bool and 'returned' on false. I put this in the RunTimer function (event, in this case), but I think in C++, it'll be more readable to put this check before the RunTimer call at the end of the check-in. Otherwise, we'll end up with nested if-statements since we can see I'll need to do an IsValid check on the audio component TickSound.


With that out of the way, I know that we'll only need to add

  • TimerEnd()

  • TimerStart (UActorComponent* TriggeringActor)

That may not seem like much, and it may seem like a waste of time in setting up an interface, so let me explain my thought process.


Because we can be talking to any actor that has the ability to respond to timer events, it's simpler to use an interface and send messages to that actor. This is especially true if there are multiple actors involved (as is the case with multiple platforms responding to a timer). Using an interface prevents us from having to cast to multiple actor types whenever a timer is triggered; in short, interfaces are agnostic.


That's why my Event TimerCheckIn didn't make sense. We know that we'll always talk to the timer component, and we clearly have a reference to it (or to its owner), so there's no need to implement the interface at all in the component. We only need to message it.



So, we get the interface written. We'll tag these as BlueprintImplementableEvent for now, but we'll need to set them as BlueprintNativeEvent later when we want to implement it in C++ classes.


Developing C++ Component


So the first thing we'll do is convert all events, custom functions, and variables into C++. That'll just be bare-bones for now so that we can get most of this stuff working as desired.


PrepareTimer(TArray<AActor*> TargetActors)

This function sets up the timer's initial conditions and works as a sort of "begin play", but we need to get the target actors from the owner, so we need to wait until the owner has begun play, and then pass the targets from the owner into the timer.

I needed to add a delegate for the TimerReset as we use it to trigger behavior in owners In Blueprint, we set a value to our array which we then use to set up the initial check-in map. We can skip that step and simply use the incoming array to set up the initial keys for our check-in map with values of false. The SetDefaults() event looks a bit redundant, so we'll remove it and simply add all the remaining code to its own set of functions that we'll call in PrepareTimer().


We also need to set up a function for prepping the sound cues as shown in this.Blueprint. That required getting GameplayStatics (for spawning the cue into a component), adding variables for the component and sound cue I'm using, and then including the header file for the Audio Mixer Blueprint Library to prime the sound cue.

The do-once is easy. It's just a member bool used as the condition for an if statement. The macro format of this opens up, and it looks a lot more complicated.

In C++, we can make that a lot simpler, with bool bSoundCompHasInit = false; in the header file as a private member variable. Then we can wrap this in a negative if-statement.


Nice and easy. The rest of the setup is pretty boring, but if you want to dive into the .cpp and .h files to see them in depth, you can download them here.


The bit that is interesting is translating the delay nodes into C++; they don't exist in the same way. I had to use timer handles and a bit of grouping to get it to work as expected. The original Blueprint looked like this in the TimerRun().


Delays were crucial to the sound design of the timer, so I needed to make use of Timer Handles. I also needed to group these nodes into two different functions to call on the timers, shown in red and blue.



Now that Set Timer by Function Name seems redundant now, but it works the same way as setting timers in the timer manager, so I used that as a starting point.


Messaging the Interface


This wasn't as straight-forward as it seemed to be. In Blueprints, interface messaging is a piece of cake; you drop in a (Message) node, throw in a target, and plug in your parameters, like this:



In C++, it's not quite as simple. In BP, we 'implement' the interface into the class. In C++, the interface is 'inherited' as though it were a parent. Because of that, you have to cast the actor to the interface in order to access the interface's functions via the actor.


The problem is how you would otherwise intuitively do a cast. The example below shows A, the working method, and B, the presumed method (based on conventional casting).


In B, we cast to the interface, then use the if statement as a pointer safety check before calling the functions off the new pointer.


The problem is, even though the cast works, the pointer check doesn't. That's because UInterface classes are abstract classes; they don't get instantiated conventionally so there's no pointer to check. Any if check is going to return false, so the statement won't fire. That's why method A is the working method.


We check to see if the actor implements the interface, then if it does, we cast to the interface and call the function on it.


Difficulties

One of the big struggles was getting the interface messages to fire. There were a few issues with how UE4 handles C++ interfaces.


  1. The interface has to be marked as Blueprintable in the UINTERFACE macro. as interfaces in C++ aren't blueprintable by default.

  2. This issue is compounded by the fact that, even if it's not mark as Blueprintable, the event implementation is still available as a Blueprint node, it just won't fire.

  3. Casting to an interface won't return a proper instance pointer.

  4. Because of how C++ dictates interface use, you have to ensure interface implementation of the actor as the condition for your cast and message.

  5. Interface messages aren't called like other class functions.

  6. Calling a message directly [InterfaceActor->StartTimer()] will call an exception. You have to call [InterfaceActor->Execute_StartTimer()]

Online resources seemed to be focused more on interfaces that had return types rather than the specific implementation of a C++ interface into a Blueprint, so I had to piece the issues together.

Results

Once all of the BPs had been readjusted to call / respond to the interface and events of the C++ component, the behavior appeared to be exactly the same. The nativization will have a positive impact on memory usage and thread times.


More importantly, though, it turned out to be a learning experience.

11 views0 comments

Recent Posts

See All