When designing levels in Unreal Engine, one common need is to detect when characters enter or exit certain defined spaces or paths. A versatile approach is to use spline-based volumes, which can dynamically shape collision and trigger interactions based on splines.
In this blog post, we'll dive into how we've implemented spline-based collision volumes using C++ in Unreal Engine, allowing for both flexible placement and powerful debugging capabilities.
Understanding the AFMSplineVolumeActor Class #
At its core, our spline-based volume functionality revolves around a custom AFMSplineVolumeActor
class which uses the built-in USplineComponent
to dynamically generate collision boxes along a spline, enabling us to easily define complex trigger zones.
Here's a brief overview of its main features:
- Spline Component: Defines the shape and path of the collision volume.
- Collision Boxes: Generated dynamically along the spline path.
- Character Overlap Handling: Detects when characters enter or leave the volume.
- Debug Visualization: Visualize generated boxes and overlap interactions at runtime.
Key Components and Logic #
Generating Collision Boxes Along the Spline #
The heart of this actor is the GenerateCollisionBoxes()
method. It does quite a bit, but really you can boil it down to:
Clearing out any existing collision boxes to get back to a sane state. The "Clean up" phase.
1void AFMSplineVolumeActor::ClearCollisionBoxes()
2{
3 for (int32 i = CollisionBoxes.Num() - 1; i >= 0; --i) {
4 if (CollisionBoxes[i]) {
5 CollisionBoxes[i]->DestroyComponent();
6 }
7 }
8 CollisionBoxes.Empty();
9}
Creating collision boxes along the spline.
We calculate the required number of boxes based on the spline length and spacing (BoxSpacing
):
1const int32 NumBoxes = FMath::CeilToInt(SplineLength / BoxSpacing) + 1;
2for (int32 i = 0; i < NumBoxes; ++i) {
3 float Distance = FMath::Clamp(i * BoxSpacing, 0.0f, SplineLength);
4 FVector Location = SplineComponent->GetLocationAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::World);
5 SetupCollisionBox(NewBox, Location);
6}
Fill the interior for closed loops.
1if (SplineComponent->IsClosedLoop()) {
2 FVector MinBound, MaxBound;
3 CalculateSplineBounds(MinBound, MaxBound); // bounding box around spline
4
5 for (float X = MinBound.X; X <= MaxBound.X; X += BoxSpacing) {
6 for (float Y = MinBound.Y; Y <= MaxBound.Y; Y += BoxSpacing) {
7 FVector TestPoint(X, Y, SplineCenter.Z);
8 if (IsPointInsideSpline(TestPoint)) {
9 SetupCollisionBox(NewBox, TestPoint);
10 }
11 }
12 }
13}
Handling Overlap Events #
Collision boxes trigger events when characters overlap with the volume. We use a simple system that worked quite well:
- Keep a counter when entering one of the collision boxes we've generated and decrement the counter when we leave.
The logic is pretty easy.
- When we go from 0 → 1 we fire a custom
OnCharacterEnteredVolume
. - When we go from 1 → 0 we fire a custom
OnCharacterLeftVolume
.
1UFUNCTION(BlueprintImplementableEvent)
2void OnCharacterEnteredVolume(ACharacter* OverlappingCharacter);
3
4UFUNCTION(BlueprintImplementableEvent)
5void OnCharacterLeftVolume(ACharacter* OverlappingCharacter);
Powerful Debugging Tools #
To simplify troubleshooting and visual feedback during development, I've included some custom CVar commands and a custom macro for debug logging. It's surprising how much a little thought into DX tools can help you without overwhelming your logging.
1DEFINE_LOG_CATEGORY_STATIC(LogFMSplineVolume, Log, All);
2
3static TAutoConsoleVariable<int32> CVarDrawSplineVolumeDebug(
4 TEXT("SplineVolume.DrawDebug"),
5 0,
6 TEXT("Draw debug visualization for AFMSplineVolumeActor instances.\n")
7 TEXT("0: Use Actor's 'bDrawDebugVisuals' property\n")
8 TEXT("1: Force visualization ON for all instances"),
9 ECVF_Cheat);
10
11static TAutoConsoleVariable<int32> CVarLogSplineVolumeDebug(
12 TEXT("SplineVolume.LogDebug"),
13 0,
14 TEXT("Control logging for AFMSplineVolumeActor instances.\n")
15 TEXT("0: Disable logging\n")
16 TEXT("1: Enable logging"),
17 ECVF_Cheat);
18
19#define LOG_SPLINE_VOLUME(Verbosity, Format, ...) \
20 if (CVarLogSplineVolumeDebug.GetValueOnGameThread() > 0) \
21 { \
22 UE_LOG(LogFMSplineVolume, Verbosity, Format, ##__VA_ARGS__); \
23 }
Debugging can be toggled directly in the editor or via runtime console commands, helping level designers quickly validate their setups.
Example Usage #
To utilize the spline volume actor, simply place it in your level, adjust the spline path as needed, and tweak parameters such as volume dimensions, box spacing, and debug visuals.
Here’s an example scenario where this actor excels:
- Creating defined paths or zones for player interactions (like stealth detection, racing lines, or tutorial triggers).
- Dynamically adjusting game mechanics based on the player’s location along spline-defined paths.
⚡Bonus Tip: You can use the Modeling Mode's Draw Spline feature to target replacing the spline as it comes with some handy drawing modes like Free Draw.
Performance Considerations #
- Transient Components: Collision boxes are transient, regenerated on construction or game start. They don’t persist, ensuring minimal memory overhead.
- Efficient Overlap Checks: Overlaps are efficiently handled with counters, reducing unnecessary event calls.
- Conditional Ticking: The actor only ticks when debug visuals are enabled, ensuring minimal runtime cost.
Summary #
By harnessing the flexibility of spline-based collision volumes, we've created a powerful tool that provides level designers and developers a dynamic and performance-efficient solution for handling complex spatial interactions.
This approach significantly streamlines both the creation and debugging of game mechanics tied to spatial volumes, empowering developers to rapidly prototype and refine gameplay ideas.