Unreal 5 C++ – Crypt Rider


A first-person puzzle game prototype built in UE5 C++ where the player grabs and places physics objects to solve environmental puzzles. The game loop is simple — pick up the key, carry it to the pedestal, the door opens — but the implementation decisions behind three custom components make it interesting to dig into.

📺 Demo Video  |  🔗 GitHub Repository  |  📖 Learning Source


The three components

The entire puzzle system lives in three custom components. They are intentionally decoupled from each other — no component knows about the others directly, and they communicate through a shared tag on actor instances.

UGrabberComponent is a USceneComponent attached to the first-person camera. That inheritance choice is deliberate: by being a USceneComponent, the grabber has its own world transform. GetComponentLocation() and GetForwardVector() return the camera’s exact position and look direction without any manual conversion. Every calculation — the sphere sweep start point, the physics handle target position — derives directly from that transform.

UTriggerComponent is a UBoxComponent that monitors its overlap volume each tick and drives a UMoverComponent based on whether a valid key actor is resting inside it.

UMoverComponent is a pure UActorComponent attached to door or gate actors. It interpolates the actor between its spawn location and a configured offset position. It has no knowledge of what drives it — UTriggerComponent holds a reference to it set at runtime via SetMover().


Grabbing: sphere sweep over line trace

For a puzzle game, a line trace is too punishing. The player has to aim pixel-precisely at the surface of an object, which becomes frustrating with irregularly shaped keys or statues that have convex geometry. A sphere sweep with a configurable GrabRadius creates a forgiveness zone around the aim point:

cpp

const FCollisionShape Sphere = FCollisionShape::MakeSphere(GrabRadius);
World->SweepSingleByChannel(HitResult, Start, End, FQuat::Identity, Channel, Sphere);

The sweep uses a custom trace channel (ECC_GameTraceChannel2, set up in Project Settings) so only objects explicitly assigned to the “Grabbable” object type respond to the grabber — environmental geometry, walls, and floors are excluded without needing per-object collision profile overrides.

When a hit is found, the sequence is: enable physics simulation on the component (in case it was disabled), wake its rigid bodies, add the "Grabbed" tag to the owning actor, detach it from any parent, and hand it to UPhysicsHandleComponent. The detach step is necessary — an actor still attached to a parent would fight the handle’s position updates every frame.


The physics sleep problem

Every tick, UpdatePhysicsHandle() moves the handle target to HoldDistance units in front of the camera and calls WakeAllRigidBodies() on the grabbed primitive. That wake call is not optional.

Unreal’s physics engine puts rigid bodies to sleep after a period of low velocity to save simulation cost. A sleeping body ignores position updates from UPhysicsHandleComponent entirely — it stops responding to the handle constraint and freezes in mid-air even though the handle target keeps moving. The first time you encounter this without knowing why, the grabbed object just stops following the camera for no apparent reason. Waking the body each tick keeps the physics engine engaged and processing the handle constraint continuously.


The “Grabbed” tag as a decoupling mechanism

The trigger exclusion is the most architecturally interesting part of the system. UTriggerComponent::GetOverlappingActorWithTag() looks for actors that carry TagName (the puzzle key tag) AND do NOT carry "Grabbed":

cpp

if (!OverlappingActor->ActorHasTag(TagName)) { continue; }
if (OverlappingActor->ActorHasTag("Grabbed")) { continue; }
return OverlappingActor;

The second check is what prevents the player from hovering the key above the pressure plate while still holding it and having the door open. The key must be physically released onto the trigger — the puzzle requires committing to a decision.

The cleaner alternative would be to store a reference to the currently grabbed actor in UTriggerComponent and check against it directly. But that would couple the trigger to the grabber at the class level. The tag approach keeps both components fully independent: UGrabberComponent writes "Grabbed" when it picks something up, UTriggerComponent reads it when scanning overlaps, and neither component includes the other’s header. The actor tag array is the shared communication channel.


Pinning the key on the pedestal

When a valid key is accepted by the trigger, two things happen to it:

cpp

ActorRoot->SetSimulatePhysics(false);
ActorRoot->AttachToComponent(this, FAttachmentTransformRules::KeepWorldTransform);

Physics simulation is disabled so the key doesn’t roll off the pedestal when the door starts moving and creates vibrations in the level. Attaching it to the trigger component keeps it locked in position relative to the trigger volume. If the trigger were on a moving platform or the puzzle required multiple stages, the key would move with it correctly.


Door movement: constant speed over ease-out

UMoverComponent uses VInterpConstantTo rather than VInterpTo. The difference matters for puzzle games. VInterpTo eases out exponentially — it slows down as it approaches the target and asymptotically approaches but never quite reaches it in finite time. That makes door timing unpredictable from a designer’s perspective: the door always takes slightly longer than expected, and the exact moment it “finishes” opening is undefined.

VInterpConstantTo moves at a fixed speed regardless of remaining distance. Speed is derived as MoveOffset.Length() / Time, so the door always takes exactly Time seconds to travel the full offset regardless of its current position. If the door is partway open when the trigger deactivates and it starts closing, it closes at the same speed — the Time parameter remains meaningful throughout.


SetMover() instead of a serialized reference

UTriggerComponent gets its UMoverComponent reference via a Blueprint-callable SetMover() call rather than through a serialized UPROPERTY asset reference. This means the trigger doesn’t need to know at edit time which specific mover it will drive — the wiring happens at runtime in the level Blueprint or the door actor’s BeginPlay. One trigger can be rewired to any mover in the level without modifying either asset. For a larger puzzle game with many triggers and movers, this composability matters.


Engine version: Unreal Engine 5.0

Leave a comment

Create a website or blog at WordPress.com

Up ↑