Log 14: Not-So-Quick-Time-Events
- Kassandra McCormack
- Jan 2, 2024
- 5 min read
As the title implies, QTEs wound up not only living up to my expectations of difficulty to implement, but exceeded them. Sure, I found a couple of tutorials for them and they pointed me in the right direction, but none of them were applicable to my situation, or used really inefficient scripting. But, I got it eventually:

There's some fine tuning with the timings that I still need to go over, but it is good enough that I've stared moving on to the rewards for doing the QTEs correctly (but those are for next log). For now let's go over my difficulties and successes with implementing the quick-time-events.
The first thing I did was look up tutorials on quick time events, because why should I re-invent the wheel if I don't need to? Unfortunately, most of them I wound up discarding as they were either too tied to the specific example, or were messes of spaghetti code that seemed inefficient and messy to track in a larger project. I did find one that pointed me in the right direction: using macros. Quick-time-events are repeating actions that need to take place over time, which is what macros are, so it only makes sense.
Unfortunately that tutorial used a delay node inside the macro and I was hoping to avoid using those, so I did some more digging for timer like macros that didn't rely on it. Eventually I stumbled on another macro that used a "Gate" node and a looped logic, which seemed good except it also relied on a "Delay Until Next Tick" node. At that point I gave up because work needed to continue so I implemented a version of the "Delay Until Next Tick" and "Gate" nodes in combination with some of the logic from the first QTE macro tutorial.

What my macro does is increment a timer every frame and then analyze a reference to a QTE struct. If the struct says the QTE was successfully completed, the macro executes a "Success" exec node, if the timer has elapsed or an incorrect input received it executes a "Timer Elapsed" or "Incorrect Input" exec nodes, respectively. But, I can't receive input events from within a macro, so how did I manage that? Well, that's where the struct comes into play. Because the struct was passed as a reference I can manipulate it outside the macro in the event graph and it will still influence the inside of the macro as well.
For the morphing QTEs I decided to disregard incorrect button presses (except to possibly register some kind of polish at a later point) and not punish the player in that way, so that simplified my input analysis. I made a new Input Mapping Context (IMC) with new Input Actions (IA) to cover the four QTEs, and when they start I put that IMC into the player's controller while removing the others. Thus the only input the controller is looking for is those four buttons. I then have those input events call a single event in the character blueprint and pass the IA triggered so I can analyze it there by comparing it to the IA required by the QTE struct, then I can set the QTE struct to success or not. In the future if I want to register incorrect inputs and either fail the QTE or just put some kind of polish on it, I'm going to have to redo this to listen to any kind of inputs, but now I only need to worry about a small chunk of code rather than the whole of the QTE algorithm. Yay, modularity!
So, with all this groundwork all that was left was create a timer widget to display on the screen and everything would be good to go. Oh how wrong I was.
The timers would not display the correct buttons, NO input was registering what-so-ever, and the timer was not counting down/animating. Good, everything is broken!
I started with why the widget was not showing the correct buttons, as that relied on an input display asset that I bought in the Unreal Marketplace and I figured that couldn't be malfunctioning so it had to be something else. It turns out, changing what image is displayed in an Image component is more complicated than it should be. Not only did I have to create a Dynamic Material Instance (which I already figured and implemented), but the node "Set Brush from Material" just...doesn't work as advertised. I only found this out after a couple hours of researching every step in the process. Eventually I found out that I need to use the regular "Set Brush" node with all the pins just pulling from the old brush, except the [Brush Image] pin which needs to take in the new Dynamic Material.

After that the timer was relatively easy, I was just not getting the correct function called where I thought it was. Sometimes it is just user oversight of something ridiculously simple, and I am a big enough person to admit to and take ownership of my mistakes.
Now, the lack of input reads, that one took me days to figure out. I even broke down and wound up making my first post on the Unreal Development Help Forum begging for someone to provide my any insight, because my research turned up NOTHING. It took me days of testing various changes to my inputs, and banging my head against the wall, but eventually I discovered that when you change IMCs, they aren't available that frame and if you try to access them, it breaks the input reader. Fortunately the way I figured that out was also the solution: one forum post I, eventually, found mentioned a "Request Rebuild Control Mappings" node. At first I tried using that, but it wasn't helping. Then, I noticed an [Options] pin on the node, and I split that. And there, oh so innocently, was a check box for [Force Immediately], and THAT was how you get newly activated IMCs to be available not only on the same frame, but immediately after the call to add them. It may seem obvious in hindsight, but if I hadn't stumbled on that forum post I never would have solved the problem.

The only thing remaining to do with the QTEs is put some kind of response polish on the widget so it doesn't JUST disappear when input is read, it should flash or something. But after the headache they gave me, I'm putting that off for a bit and working on something else so I can hopefully re-center myself.
Comments