Unreal Engine 5.8 tutorial

Part 5 — Your first 3D game (third-person action)

Build a third-person action prototype in Unreal Engine 5.8: character movement, Enhanced Input, Animation Blueprints, attack montages, the damage path, and a simple enemy.

Goal: a controllable 3D character that moves, jumps, dashes, has a follow camera, and drives the full WISP core loop — motes that restore Glow, beacons you relight (and Lumen reacts), and Shades that chase and drain you. We lean on the Third Person template you created in Part 0 as a base and extend it, building everything in a new map, L_Hollow_3D, inside the same Wisp project.

This part is a reproducible walkthrough: every Blueprint, variable, default value, and node sequence is spelled out. Work top-to-bottom — each sub-section ends with a Test it check so you never build three systems on top of a broken one. By the end you have BP_Wisp, BP_Mote, BP_Beacon, BP_Shade, and BP_HollowGameMode wired into one playable zone.

Before you start. Commit your work from Part 4 first (git add . && git commit -m "part 4 done"). Then in the Content Browser create a new Level at Content/_Wisp/Maps/L_Hollow_3D (right-click → Level, or File → New Level → Basic). All Blueprints below live in Content/_Wisp/Blueprints/.

5.1 Anatomy of the third-person character

Open BP_ThirdPersonCharacter (under the template’s content; in 5.8 the Third Person template ships it as a Blueprint). Its component tree:

CapsuleComponent (root, collision + the "body")
+-- ArrowComponent (editor-only, shows forward)
+-- Mesh (SkeletalMesh, the visible character) + its AnimBlueprint
+-- SpringArm ("CameraBoom" - a telescoping arm that handles camera collision)
|   +-- Camera (FollowCamera)
+-- CharacterMovementComponent (not in the visual tree - it's a non-scene component)
  • The SpringArm auto-pulls the camera in when a wall is between camera and character — that’s why third-person cameras don’t clip through geometry. You set its length, socket offset, and enable Camera Lag for smooth follow in 5.3.
  • CharacterMovementComponent (CMC) gives you walking, falling, jumping, crouching, swimming, flying with networked prediction built in. You tune Max Walk Speed, Jump Z Velocity, Air Control, Gravity Scale, etc. on its Details panel.
  • The Character base class (not Pawn) is what gives you the capsule + CMC + a mesh slot out of the box. The Wisp is a Character in 3D (it was a PaperCharacter in Part 3); the API you use here — Add Movement Input, Jump, Launch Character — is identical.

5.2 Lab: create BP_Wisp from the Third Person base

We do not mutate the template character — we make our own so the template stays as a reference. There are two clean ways; pick one:

  • Reparent (preferred, keeps the camera/input wiring): In the Content Browser duplicate BP_ThirdPersonCharacter into Content/_Wisp/Blueprints/ and rename the copy BP_Wisp. You inherit the SpringArm, Camera, and the template’s Enhanced Input graph for free, then trim/extend it.
  • From scratch: Right-click → Blueprint Class → Character, name it BP_Wisp, and add a SpringArm + Camera yourself (slower; only do this if you want to understand every node).

Use the duplicate route. Open BP_Wisp and add these variables (My Blueprint panel → Variables → +). Set the Default Value in the Details panel after you compile once:

VariableTypeDefaultPurpose
GlowFloat100.0Current light/health. Run ends at 0.
MaxGlowFloat100.0Clamp ceiling for Glow (motes can’t overfill).
DashStrengthFloat1500.0Launch impulse magnitude for the dash.
DashCooldownFloat1.0Seconds before you can dash again.
bCanDashBooleantrueGate so dash can’t spam (reset by a Timer).
LastMoveDirVector(1,0,0)Last non-zero move direction, used when the stick is centered at dash time.

Now set the Game Mode to spawn the Wisp as the player. In 5.7 you make BP_HollowGameMode and set its Default Pawn Class to BP_Wisp; for now you can verify movement by setting World Settings → GameMode Override later, or just temporarily set BP_Wisp as Default Pawn in Project Settings → Maps & Modes.

Test it. Compile and Save. In L_Hollow_3D, drag a few cubes/floor from Place Actors (or use the Starter Content SM_ meshes) so there’s ground and walls. Set BP_Wisp as the player pawn, press Play (PIE). You should spawn and stand on the floor — movement and camera come online in 5.3.

5.3 Input → movement and camera (Enhanced Input, 3D)

The duplicated graph already wires IA_Move, IA_Look, and IA_Jump through an IMC_Default (or IMC_Player) mapping context. Understand and confirm it; then you add IA_Dash.

How the context gets applied (this is in BeginPlay):

Event BeginPlay
  -> Get Controller -> Cast To PlayerController
  -> Get Enhanced Input Local Player Subsystem (from that controller's Local Player)
  -> Add Mapping Context (Mapping Context = IMC_Default, Priority = 0)

Move (camera-relative): IA_Move is an Axis2D (Vector2D). On its Triggered event:

  • Get the Control Rotation (the camera’s aim), Break Rotator, and rebuild a yaw-only rotator Make Rotator (Pitch=0, Roll=0, Yaw=ControlYaw) — so movement follows the camera, not the camera’s pitch.
  • Get Forward Vector of that yaw rotator → Add Movement Input with Scale = Action Value Y.
  • Get Right Vector of that yaw rotator → Add Movement Input with Scale = Action Value X.
  • Also stash direction for the dash: build a world move vector (Forward × Y) + (Right × X), and if its length > 0, Normalize it and Set LastMoveDir.

Look: IA_Look (Axis2D) → Add Controller Yaw Input (Action Value X) and Add Controller Pitch Input (Action Value Y). Jump: IA_Jump StartedJump; CompletedStop Jumping.

Rotation settings (the classic third-person feel) — select the CharacterMovement component and the BP_Wisp Class Defaults:

WhereSettingValue
CharacterMovementOrient Rotation to Movementtrue (character turns toward where it moves)
CharacterMovement → Rotation RateYaw540 (snappy turning)
Class Defaults (Pawn)Use Controller Rotation Yawfalse
Class Defaults (Pawn)Use Controller Rotation Pitch / Rollfalse
CharacterMovementMax Walk Speed500
CharacterMovementJump Z Velocity600
CharacterMovementAir Control0.35

SpringArm + FollowCamera settings — select CameraBoom (SpringArm) and tune for a floaty orb-cam:

ComponentSettingValue
SpringArmTarget Arm Length450.0
SpringArmSocket Offset(0, 0, 60) (raise the framing a bit)
SpringArmEnable Camera Lagtrue
SpringArmCamera Lag Speed10.0
SpringArmEnable Camera Rotation Lagtrue
SpringArmCamera Rotation Lag Speed10.0
SpringArmUse Pawn Control Rotationtrue (mouse/right-stick orbits the camera)
FollowCameraUse Pawn Control Rotationfalse (the arm drives rotation, not the camera)

Test it. Compile, PIE. WASD / left stick moves relative to the camera; the Wisp rotates to face travel. Mouse / right stick orbits the camera with a smooth lag. Space jumps. If movement feels like it ignores the camera, you forgot the yaw-only rotator; if the character snaps to the camera even when standing still, Use Controller Rotation Yaw is still true.

5.4 Lab: the Dash (IA_Dash + Launch Character + cooldown)

The dash is WISP’s only special move — a short burst in the move direction, gated by a cooldown. Create the input asset first.

  1. In Content/_Wisp/Input/ (create the folder) right-click → Input → Input Action, name it IA_Dash. Leave Value Type = Digital (bool).
  2. Open your mapping context IMC_Default → add a mapping for IA_Dash → bind a key (e.g. Left Shift) and the gamepad Right Shoulder.

In BP_Wisp, add the IA_Dash event (right-click → search “IA_Dash”). On its Started pin:

EnhancedInputAction IA_Dash (Started)
  -> Branch (Condition = bCanDash)
       True:
         -> Set bCanDash = false
         -> (compute dash direction, see below) -> Launch Character
              Launch Velocity = DashDir * DashStrength
              XY Override = true, Z Override = false
         -> Set Timer by Function Name
              Function Name = "ResetDash", Time = DashCooldown, Looping = false

Compute the dash direction (use the live input direction if the stick is held, otherwise the last move direction so a standing dash still goes somewhere):

  • Take LastMoveDir (already kept up to date by IA_Move in 5.3). Project it flat: Make Vector(X = LastMoveDir.X, Y = LastMoveDir.Y, Z = 0)Normalize → this is DashDir.
  • Multiply DashDir by DashStrength → feed into Launch CharacterLaunch Velocity. Tick XY Override so the dash replaces horizontal velocity; leave Z Override off so you don’t cancel a jump.

Add a custom event ResetDash that simply does Set bCanDash = true. (The Timer above calls it by name after DashCooldown seconds.)

Custom Event ResetDash
  -> Set bCanDash = true

Test it. PIE, hold a direction and tap Shift — the Wisp lunges. Immediately tap again: nothing (gated by bCanDash). Wait ~1s and it works again. Dash while standing still: it fires along your last facing. If you fly upward, you accidentally enabled Z Override.

5.5 Lab: BP_Mote (collectible that restores Glow)

The Mote is a floating pickup. Zero art: use a Starter Content sphere with an emissive material instance, or a simple Sphere mesh tinted cyan.

  1. New Blueprint Class → ActorBP_Mote in Content/_Wisp/Blueprints/.
  2. Add a Sphere Collision component, make it the root (drag it onto DefaultSceneRoot). Set Sphere Radius = 60. Set Collision Presets = OverlapAllDynamic and make sure Generate Overlap Events = true.
  3. Add a Static Mesh child: assign SM_Sphere (Starter Content) or the Engine basic Sphere; scale to 0.4. Assign an emissive material (duplicate Starter Content’s glow material into MI_MoteGlow, set Emissive Color to bright cyan). Set its Collision = NoCollision (the Sphere Collision handles overlap).
  4. Add a Float variable GlowAmount, default 20.0.
  5. Add an Audio reference: pick a short Starter Content cue for PickupSound (a SoundBase variable), or use Play Sound at Location with a chosen cue.

Graph — On Component Begin Overlap (Sphere):

On Component Begin Overlap (Sphere)
  -> Cast To BP_Wisp (Other Actor)
       Success:
         -> (on the Wisp) Set Glow =
               Clamp( Glow + GlowAmount, 0, MaxGlow )
         -> Play Sound at Location (PickupSound, Get Actor Location)
         -> Destroy Actor (self)

Put the Glow math inside a function on BP_Wisp for reuse — add AddGlow(Amount: Float) that does Set Glow = Clamp(Glow + Amount, 0, MaxGlow). Then the Mote just casts and calls AddGlow(GlowAmount). (You’ll reuse AddGlow with a negative amount for Shade drain in 5.7.)

Optional polish: add a slow spin in Event TickAdd Actor Local Rotation (Yaw = 90 × Delta Seconds) — and a small vertical bob.

Test it. Drag 3–5 BP_Mote into the level. First lower your Wisp’s starting Glow (temporarily set the Glow default to 40). PIE, walk through a mote: it plays a sound and vanishes, and Glow rises but never exceeds MaxGlow. Add a temporary Print String of Glow after AddGlow to confirm the clamp.

5.6 Lab: BP_Beacon (relight → Lumen reacts)

The Beacon is the heart of the loop: a dark mesh with a switched-off light and a trigger. Walk into it (or press Interact) and its light turns on — because the level uses Lumen, the surrounding geometry visibly brightens with real-time global illumination.

  1. New Blueprint Class → ActorBP_Beacon.
  2. Add a Static Mesh (e.g. SM_Pillar / a cylinder / a Starter Content lamp). This is the visible beacon body; leave its collision as BlockAll so you can’t walk through it.
  3. Add a Point Light component as a child, positioned at the top of the mesh. Set Visible = false in its Details (Rendering → Visible) so it starts dark. Give it a warm color, Intensity = 8000, Attenuation Radius = 1200, and tick Cast Shadows. Leave Mobility = Movable (so it can toggle at runtime and feed Lumen).
  4. Add a Box Collision trigger around the base: Box Extent = (200, 200, 150), Collision Presets = OverlapAllDynamic.
  5. Add a Boolean bIsLit (default false).
  6. Add an Integer BeaconID and mark it Instance Editable (the open-eye toggle next to the variable). Give each placed beacon a unique value (0, 1, 2…) in its instance Details — Part 6’s save system references each beacon by this id.

Two ways to relight — pick one or support both:

  • On overlap (simplest): On Component Begin Overlap (Box)Cast To BP_Wisp → call a Relight custom event.
  • On interact (nicer): create IA_Interact (Digital), map it to E / gamepad Face Button Bottom. The Wisp keeps an OverlappingBeacon reference (set on its own overlap begin/end with a beacon); on IA_Interact (Started) it calls Relight on that beacon if valid.

Split the lit logic into two functions. Part 6’s save/load needs to restore a lit beacon silently (no swell sound, no score) while the player’s relight action plays the sound and bumps the counter. So make ApplyLitState (the pure visual state change) and Relight (the player action that calls it). A loaded/saved beacon calls ApplyLitState directly; the player calls Relight.

Graph — ApplyLitState (function on BP_Beacon, no sound, no score):

Function ApplyLitState
  -> PointLight -> Set Visibility (New Visibility = true)   // Lumen now lights the area
  -> Set bIsLit = true
  -> (optional) Set Emissive on the mesh's material instance higher

Graph — the Relight event (the player action, on BP_Beacon):

Custom Event Relight
  -> Branch (Condition = NOT bIsLit)          // ignore double-relights
       True:
         -> ApplyLitState                      // turns the light on, sets bIsLit
         -> Play Sound at Location (swell cue, Get Actor Location)
         -> Get Game Mode -> Cast To BP_HollowGameMode
              -> Notify Beacon Lit

You make Notify Beacon Lit a function/event on the Game Mode in 5.8. For now stub it (a Print String "Beacon lit") so the Beacon compiles; you wire the real counter next. When Part 6 loads a save, it walks the placed beacons by BeaconID and calls ApplyLitState on the ones that were lit — silently, with no replayed sound and no double-counted score.

Lumen note. The Lumen real-time GI you see depends on a Movable/Stationary light and Lumen being the active Global Illumination + Reflections method (Project Settings → Rendering, default in 5.8). If the room doesn’t brighten, confirm the Point Light is Movable and that Lumen (not “None” or a baked-only setup) is selected. Lumen Lite exists for lower-end targets but is not needed here.

Test it. Place one BP_Beacon in a darkened part of the level (drop the directional/sky light intensity, or build a small room). PIE, walk into its trigger (or press E): the point light snaps on and the nearby walls/floor brighten as Lumen bounces the new light. The Print String "Beacon lit" fires exactly once even if you re-enter the trigger.

5.7 Lab: BP_Shade (chasing enemy that drains Glow)

The Shade is a Character driven by an AIController. In Part 6 it graduates to a Behavior Tree; here we use the minimal version — an AI MoveTo the Wisp on a timer — which is exactly the kind of “good enough now, upgrade later” scoping the capstone teaches.

Navigation first. The AI can only path on a navmesh:

  1. From Place Actors → Volumes, drop a NavMeshBoundsVolume and scale it to cover your whole play area (X/Y wide, Z tall enough to include the floor + character height).
  2. Press P in the viewport to toggle the green navmesh overlay. Green = walkable. If you don’t see green, the volume doesn’t enclose your floor, or the floor isn’t marked walkable.

Create the Shade:

  1. New Blueprint Class → CharacterBP_Shade. The Character’s inherited Mesh is a SkeletalMeshComponent — it cannot take a static Cone, so don’t try to “swap” it to one. Pick one: either keep the mannequin skeletal mesh and give it a dark, slightly emissive material (e.g. MI_ShadeDark) so it reads as a shadow blob, or hide the inherited Mesh entirely (set its Rendering → Visible = false). Then add a separate Static Mesh component (a Cone or Sphere) under the capsule for the shadow-blob look — no rig required.
  2. Add a Float DrainPerSecond, default 15.0.
  3. Add a Bool bTouchingWisp (default false) and an object reference TargetWisp (type BP_Wisp).
  4. Add a dedicated Sphere Collision component (name it DrainSphere) for the contact drain — do not reuse the capsule. The default Pawn collision profile blocks other Pawns, and ticking Generate Overlap Events alone does not change that, so the player would never overlap the capsule. On DrainSphere set Collision Enabled = Query Only (No Physics Collision), set its response to Overlap for the Pawn channel and Ignore for all others, and ensure Generate Overlap Events = true.
  5. Create an AIController: New Blueprint Class → AIControllerAIC_Shade. In BP_Shade Class Defaults set AI Controller Class = AIC_Shade and Auto Possess AI = Placed in World or Spawned.

Chase logic — in BP_Shade Event Graph:

Event BeginPlay
  -> Get Player Character (index 0) -> Cast To BP_Wisp -> Set TargetWisp
  -> Set Timer by Event (Time = 0.5, Looping = true) -> [ChaseTick]

Custom Event ChaseTick
  -> Get AI Controller (self) -> Cast To AIC_Shade
  -> AI MoveTo
       Pawn = self
       Target Actor = TargetWisp
       Acceptance Radius = 80

Set the Shade slower than the Wisp so it’s avoidable: select CharacterMovementMax Walk Speed = 300 (vs the Wisp’s 500). Re-pathing every 0.5s is plenty for a chaser; you don’t need per-tick MoveTo.

Drain on contact. Bind On Component Begin/End Overlap on the DrainSphere you added above (not the capsule — the capsule blocks Pawns and never overlaps the player). On overlap with the Wisp, drain Glow continuously while touching:

On Component Begin Overlap (DrainSphere)
  -> Cast To BP_Wisp -> Set bTouchingWisp = true (and store TargetWisp)

On Component End Overlap (DrainSphere)
  -> Cast To BP_Wisp -> Set bTouchingWisp = false

Event Tick (Delta Seconds)
  -> Branch (Condition = bTouchingWisp)
       True:
         -> TargetWisp -> AddGlow( -DrainPerSecond * Delta Seconds )

Because AddGlow already clamps to [0, MaxGlow], the drain naturally bottoms out at 0 — where the Game Mode triggers the lose condition (5.8).

Test it. Press P and confirm green navmesh under your floor. Place 1–2 BP_Shade across the room, PIE: each Shade walks toward you and re-paths around obstacles. Stand still in contact — a Print String of Glow (after AddGlow) should tick down smoothly at ~15/sec. Dash away (5.4) and the drain stops. If a Shade never moves, the navmesh is missing or Auto Possess AI is wrong.

5.8 Lab: BP_HollowGameMode (win / lose)

The Game Mode owns the rules: how many beacons exist, how many are lit, and what ends the run. It also sets BP_Wisp as the player pawn.

  1. New Blueprint Class → Game Mode BaseBP_HollowGameMode. Set Default Pawn Class = BP_Wisp.
  2. In L_Hollow_3DWorld Settings → GameMode Override = BP_HollowGameMode. (Per-map override beats the project default and keeps your other maps unaffected.)
  3. Add Integer variables BeaconsLit (default 0) and TotalBeacons (default 0).
  4. Add two SoundBase / class references as needed for the win/lose widgets (5.9 builds them; stub for now).

Count the beacons at start, then track lit:

Event BeginPlay
  -> Get All Actors Of Class (BP_Beacon) -> Length -> Set TotalBeacons

Custom Event Notify Beacon Lit          // called by BP_Beacon.Relight
  -> Set BeaconsLit = BeaconsLit + 1
  -> Branch ( BeaconsLit >= TotalBeacons )
       True: -> [Win]

Lose on Glow ≤ 0. Drive this from the Wisp so the Game Mode stays the single decision-maker. In BP_Wisp.AddGlow, after the clamp, add: Branch (Glow <= 0) → Get Game Mode → Cast To BP_HollowGameMode → Notify Wisp Drained. Then on the Game Mode:

Custom Event Notify Wisp Drained
  -> [Lose]

// Win / Lose helpers (shared widget show):
Custom Event Win
  -> Create Widget (WBP_WinScreen) -> Add to Viewport
  -> Set Game Paused (true)  OR  Set Input Mode UI Only + Show Mouse Cursor

Custom Event Lose
  -> Create Widget (WBP_LoseScreen) -> Add to Viewport
  -> Set Game Paused (true)  OR  Set Input Mode UI Only + Show Mouse Cursor

For a fast first pass you can skip real widgets and use a single WBP_EndScreen with a Text block whose value you pass in (“HOLLOW RESTORED” vs “THE LIGHT WENT OUT”), plus a button that calls Open Level (L_Hollow_3D) to retry. Building the polished menus/HUD is Part 6’s milestone; here you just need a clear end state.

Test it. Place 3 beacons, set each Wisp/Shade default appropriately, PIE. Relight all beacons → the win widget appears and the game pauses. Restart, let a Shade drain you to 0 → the lose widget appears. Add a temporary Print String of BeaconsLit / TotalBeacons in Notify Beacon Lit to confirm the count matches the number you placed.

5.9 Optional C++ base for the Wisp

If you prefer the Glow logic in C++ (clearer diffs, faster iteration on the math, and a clean base for Blueprint to extend), make AWispCharacter the parent of BP_Wisp. This is optional — the Blueprint version above is complete — but it shows the same loop in code:

// WispCharacter.h
UCLASS()
class WISP_API AWispCharacter : public ACharacter
{
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Wisp")
    float Glow = 100.f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Wisp")
    float MaxGlow = 100.f;

    // Returns new Glow; clamps to [0, MaxGlow]; broadcasts drained at 0.
    UFUNCTION(BlueprintCallable, Category="Wisp")
    float AddGlow(float Amount);

    DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnWispDrained);
    UPROPERTY(BlueprintAssignable, Category="Wisp")
    FOnWispDrained OnWispDrained;
};

// WispCharacter.cpp
float AWispCharacter::AddGlow(float Amount)
{
    Glow = FMath::Clamp(Glow + Amount, 0.f, MaxGlow);
    if (Glow <= 0.f)
    {
        OnWispDrained.Broadcast();
    }
    return Glow;
}

Then reparent BP_Wisp (Class Settings → Parent Class → WispCharacter), bind OnWispDrained to call the Game Mode’s lose path, and keep input/dash in the Blueprint. The Mote and Shade call the same AddGlow — positive for pickups, negative for drain — whether the parent is C++ or pure Blueprint.

5.10 Assemble the zone

You now have every piece. Lay out L_Hollow_3D into a playable loop:

  • A walled arena (Starter Content walls / scaled cubes) sitting inside the NavMeshBoundsVolume — press P to confirm green coverage.
  • Dim the global lighting (lower the Directional Light + Sky Light) so unlit beacons leave the room genuinely dark — this makes each relight read dramatically through Lumen.
  • Place 3–5 BP_Beacon spread across the arena, 3–5 BP_Mote along the routes between them, and 3–5 BP_Shade at the far edges.
  • Confirm World Settings → GameMode Override = BP_HollowGameMode and that BP_Wisp is the Default Pawn. Add a Player Start in a lit safe spot.

Commit the milestone when it plays end-to-end: git add . && git commit -m "part 5: 3D core loop in L_Hollow_3D".

Exercise 5 — WISP milestone (the first 3D zone)

Build L_Hollow_3D from the Third Person base into a complete, playable core loop. The Wisp (a Character with Glow = 100, MaxGlow = 100) moves camera-relative, jumps, and dashes (IA_DashLaunch Character in the move direction × DashStrength, gated by bCanDash + a cooldown Timer). Place 3–5 BP_Mote that call AddGlow (clamped to MaxGlow) on overlap, play a sound, and destroy; 3–5 BP_Beacon with a Visible = false Point Light that you relight on overlap/IA_Interact (light turns on — watch Lumen react — and it calls NotifyBeaconLit); and 3–5 BP_Shade (Character + AIController) that AI MoveTo the Wisp on a Timer and drain Glow while touching. BP_HollowGameMode counts BeaconsLit vs TotalBeacons (win when equal) and watches Glow ≤ 0 (lose), showing the matching widget. Drop a NavMeshBoundsVolume over the arena and verify it with P. When you can dash between beacons, dodge Shades, relight the whole Hollow, and trip both the win and lose screens, you have WISP’s complete 3D core loop — the same design as your 2D prototype, now in three dimensions.