Behind The Code: Locked On Target
Devin Kelly-Sneed has been at Double Fine since 2016 and he's one hell of a programmer. Some of you might remember his Amnesia Fortnight project from 2017, Darwin's Dinner. For Psychonauts 2, Devin acted as the Player Movement and Powers Feature Lead, helping to make sure that Raz leapt with acrobatic grace and blasted bad thoughts with flashy flair. He's kindly written outlines on the implementation of a few features. Today, we're gonna have him guide us through the design of Raz's ability to target foes and other actors in the game world.
Psychonauts 2 Target Search System
The Target Search system was instrumental to Powers and Combat in Psychonauts 2. Target Search’s job is to find nearby objects in the world for other systems to interact with. Here’s a short video showing some basic combat as an example.
Here’s a quick rundown of how Target Search was used to enable that sequence:
- Player fires Psi Blast. Target Search finds the flying Regret enemy to aim at.
- Player hits the Telekinesis (TK) power button. Target Search finds the best TKable object in range to pick up.
- Player throws the object. Target Search finds a nearby Censor enemy in front of the player to use as the throw target.
- Player hits the Melee button. Target Search finds the nearby Censor enemy for Raz to align to and punch.
Visualizing a Search
A robust debug draw functionality is essential for a system that deals with spatial logic. It helps when authoring the data and when debugging undesired behavior.
This screenshot shows the debug visualization of a search for a TKable object to pick up. The green wire sphere represents the maximum world distance. The blue rectangle represents the allowed area in screen space. Each potential target is shown along with its score. And the closest object’s score is highlighted because the Target Search assigned it the highest score. Red text on two of the targets shows the reasons they were not valid targets. Seeing which conditions passed and how they scored makes it much easier to understand how it determined the best target. When the system is picking a best target that doesn’t match the design intent, we can use the debug info to narrow down the issue. Issues were usually:
- incorrect authoring/tuning
- data problem on objects involved (e.g. wrong collision type)
- bug in the conditional logic
Being able to know where the issue was without digging in with the code debugger made the process much easier and saved a lot of time.
Here you can see a visualization of the search that is determining which target to throw the box at.
Conditional System
So how do we set up and tune those searches? The P2 Conditional System is a way to author “questions” about an actor to see if it meets your criteria.
A conditional is a small bit of logic that returns true if the test passes. These can be combined to author fairly complicated queries in data. We used conditionals across the game in many systems. But we’ll focus on how this system was used in the context of Target Search.
In addition to a true/false result, conditionals also return a score that represents “how well” it passed. For most simple checks the score is just 0 or 1 for fail and pass. But for spatial queries like a world distance check, the score is 1.0 - (Distance / MaxAllowedDistance)
. So a very close object may score 0.9 while a more distant object scores 0.2. This is how the Target Search picks the best target when multiple targets pass the conditional.
Here are the conditions used when finding an object to pick up with TK.
Let’s look at the first three conditions. It’s not shown in the image, but each condition has a tunable weight that lets us assign importance.
0) Screen Box (weight 1) - check if target is on the screen in the specified rectangle
1) World Space Radius (weight 3) - check if the target is close enough in world space
2) Line of Sight (weight 0) - make sure there isn’t something between the object and Raz
In tuning this search we found that prioritizing nearby objects was more important than objects near the center of the screen. For other use cases like the charged Psi Blast aim mode we assigned much higher priority to the screen box. These videos show the effects of adjusting the area conditionals and their relative weights.
Handling Special Cases
The final two conditionals on that TK search were marked optional. This meant that the overall check could still pass, but the score could be impacted. The "Is Of Class" check shown here determines if the potential target was not Raz’s Clone character. The intent here is to drastically deprioritize targeting the Clone if anything else is nearby. This fixed an issue with constantly trying to grab the clone instead of a useful throwable object during combat.
The Target Has All Labels check was used to check for objects labeled as being particularly important, such as an object needed for completing a level objective. They were assigned a very high priority and would get picked up even when closer options were present.
System Architecture
Every Actor in the game that can potentially be a target has at least one UBaseTargetComponent
(or some derived class) attached. In the component’s InitializeComponent
function it registers itself with the target search system which was stored on the GameInstance
(Note: on future projects we moved the system to its own manager class).
The Target Search system maintained a map of lists of targets based on the type of target. To execute a search, we called FindTargets
or FindBestTarget
depending on what type of result we needed. The search was authored in UP2TargetSearchParameters
which was inherited from UDataAsset
. This contained the settings for target type, search origin, and the list of conditions. We ended up with about 70 different search assets by the time we shipped.
Conditions were implemented as children of UP2ConditionalBase
. This inherits from UObject
and uses the EditInlineNew
class specifier. Properties that hold a conditional used the Instanced property meta specifier so the author could select the exact conditional class they wanted from a dropdown to create the object. Psychonauts 2 shipped with about 90 of these conditional classes available.
Future Of The Conditional System
After shipping Psychonauts 2 we spent some time building DF Conditional to be shared by future projects. One of our priorities with the new system was trying to make the UI less confusing when authoring complex conditionals.
Here’s a test example showing some of the improved UI. Note that the conditions array has a readable description so you don’t need to expand each one to see its settings. This was implemented using the TitleProperty
meta specifier for properties in Unreal. The TitleProperty
is set to show a string that gets updated any time a property changes (using PostEditChangeProperty
).
Wrap Up
Target Search and Conditionals were a core part of how Raz’s powers worked in Psychonauts 2. The flexibility of the conditional system along with robust debug draw was incredibly powerful, especially when polishing the game. Thanks for reading, we hope you enjoyed learning how we made Raz’s powers choose their targets!