UE Beginner Capstone: Finish One Complete Coin-Collecting Game

Created: 2026-07-21

The capstone that ties every part you've learned into one game. Build a single stage where you collect 20 coins in 60 seconds, covering the coin Actor, score and remaining time in GameState, the HUD, clear/game over, high score saving, and packaging. Finishes with three additions that improve game feel.

Through the articles so far, you've touched the individual pieces: moving a character, detecting collisions, showing UI, saving data. And yet it's easy to end up with nothing you'd call "my game." Knowing how to build parts and knowing how to tie parts into one game are separate skills.

In this article, we'll build a one-stage game where you collect 20 coins in 60 seconds from start to finish. We'll set up the stage, place the coins, manage score and remaining time, show them on a HUD, split clear from game over, save a high score, and export it as an exe you can hand to someone.

A completed image of a stage lined with coins, a HUD showing score and remaining time, and a soft blue clay figure running toward a coin

What You'll Learn

  • Pinning down the spec in a single sentence before you build
  • The coin Actor (spinning, Overlap pickup, pickup effects)
  • Why score and remaining time belong in GameState, and how to implement it
  • Wiring the HUD with an Event Dispatcher
  • Clear, game over, retry, and saving the high score
  • Hands-on: three additions that improve game feel in 30 minutes

Sponsored

Decide What You're Building in One Sentence

The first thing to do is not to open the editor. It's to write the completion condition in one sentence.

Collect all 20 coins placed in a single stage within a 60-second time limit to clear. Run out of time and it's game over.

That's all. No enemies, one stage. It looks thin, but this one sentence lets you answer "should I be building this right now?" every single time.

Diagram branching from a card holding the completion condition into the parts it requires: stage, coins, score, timer, HUD, and result

Laying out the parts that sentence requires gives us this.

What we needHandled by
A character and camera you can run around withThird Person template
Collectible coinsBP_Coin (Actor)
Score and remaining timeBP_CoinGameState (GameState)
On-screen displayWBP_HUD (Widget)
Clear / game overA result panel inside the HUD
High scoreBP_CoinSave (SaveGame)

That's only four Blueprints. Deciding up front not to add any more is what gets you to the finish line.

Set Up the Stage and Player

Create a new project with Games > Third Person. Match these three settings.

SettingValue
Project DefaultsBlueprint
VariantNone
Starter ContentEnabled

Starter Content is optional, and if you forget to check it you won't have Shape_Cylinder (the coin's look) or the sound effects we use later. Do not skip this one.

This template ships with a controllable character (BP_ThirdPersonCharacter), a camera, and input settings, which saves you all the movement setup.

When you want to tune the character's movement yourself, head to Moving Characters with Character Movement Component and Third-Person Cameras with Spring Arm. Here we'll leave the template alone.

The stage doesn't need to be elaborate. Use the template's level as-is and add two or three raised platforms on the floor (Greyboxing with Modeling Mode is the fastest way to shape them). Just confirm there's a Player Start where the player should begin.

Once the project exists, it's safest to get saving and version control sorted out first, so you can recover if you break something (see Managing UE Projects with Git LFS).

Build the Coin Actor

Right-click in the Content Browser, choose Blueprint Class → Actor, and name it BP_Coin.

It has three components.

  1. Add a Sphere Collision, drag it above DefaultSceneRoot to make it the root, set Sphere Radius to 60, set Collision Presets to OverlapAllDynamic, and confirm Generate Overlap Events is checked
  2. Add a Static Mesh as a child of the Sphere and assign Shape_Cylinder (Content/StarterContent/Shapes). Set Scale to (1.0, 1.0, 0.1) to make it a thin disc and set Rotation X to 90 to stand it upright. Set Collision Presets to NoCollision
  3. Add a Rotating Movement component (search Rotating Movement when adding) and set Rotation Rate Z to 90

Adding Rotating Movement alone makes the coin spin. It's a quietly useful component that gives you rotation without writing any Tick logic (see Designing Without Event Tick, Using Timers and Events).

The key point is that the Sphere carries the collision while the visible mesh is set to NoCollision. If the hit detection matched the thin disc shape exactly, you'd miss coins when running straight into them from the side. Making the pickup volume a sphere slightly larger than the visuals is the standard move (see Simple vs. Complex Collision).

Wire Up What Happens on Pickup

Build the following graph in the event graph.

Completed node graph running from On Component Begin Overlap through Cast To BP_ThirdPersonCharacter, Spawn System at Location, Play Sound at Location, Add Coin, and Destroy Actor

Here's the same thing as pseudocode.

BP_Coin (Event Graph)

On Component Begin Overlap (Sphere)
  → Cast To BP_ThirdPersonCharacter (Object = Other Actor)
  → (on success) Spawn System at Location (System = NS_CoinPickup, Location = GetActorLocation)
  → Play Sound at Location (Sound = a short SFX, Location = GetActorLocation)
  → Get Game State → Cast To BP_CoinGameState
  → Add Coin (Amount = 1)        // A function on GameState. We build it in the next section
  → Destroy Actor (Target = Self)

The Cast To BP_ThirdPersonCharacter in the middle is there so that anything other than the player can't pick the coin up. Without it, a stray pebble flying past would make the coin disappear.

Placing the effects (particles and sound) before Add Coin is deliberate. Inside Add Coin, hitting the 20th coin runs the entire game-ending sequence in one go. Put the effects afterward and the 20th coin alone freezes the screen with no sound and no particles. Show what you want seen before you call the ending logic. It's purely a matter of ordering, but the difference is obvious when you actually play.

For audio, use Play Sound at Location. If you attach the sound to the coin with Spawn Sound Attached, the default settings mean the immediately following Destroy Actor kills the sound too, so it seems like nothing played. Remember it as: when a disappearing Actor plays a sound, choose playback that isn't bound to the Actor (see Sound Cue vs. Sound Wave).

You don't need the effect and sound assets ready yet. Leave those two nodes for later and get the whole loop working first.

Place 20 copies of your BP_Coin around the stage. Place one and hold Alt while dragging to copy it. Putting some on top of platforms and in corners makes searching for them fun.

Sponsored

Put Score and Time in GameState

This is the most design-flavored decision in the whole article.

Where should score and remaining time live? You could put them on the player character. But these two aren't "the state of that character," they're "the state of the game as a whole." You don't want the score wiped when the player dies and respawns, and you want the HUD and result screen to have exactly one place to look. That's the role of GameState.

Diagram splitting the responsibilities: GameMode holds the rules, GameState holds score and remaining time, Character holds player HP, and the HUD handles display

The division of roles is covered in detail in Understanding the Game Framework. Here, "whole-game state goes in GameState" is enough to move forward.

Create and Register the GameState

  1. Blueprint Class → pick Game State Base as the parent class and create it as BP_CoinGameState
  2. In the Content Browser, open Content/ThirdPerson/Blueprints/BP_ThirdPersonGameMode and change Game State Class in Class Defaults to BP_CoinGameState
  3. Open your level and confirm that GameMode Override in Window → World Settings is set to BP_ThirdPersonGameMode

Skip step 3 and the level falls back to Project Settings → Maps & Modes → Default GameMode. With the untouched template that happens to work, but if you create your own new level it gets a different GameMode, Cast To BP_CoinGameState silently fails, and you go straight to "collecting coins doesn't increase the score." Setting it explicitly in World Settings is safer, so the rules apply no matter which level you open.

Variables and Dispatchers

Give BP_CoinGameState the following.

NameKindTypeDefault
ScoreVariableInteger0
TimeRemainingVariableFloat60.0
TargetCoinsVariableInteger20
bFinishedVariableBooleanfalse
BestScoreVariableInteger0
CountdownHandleVariableTimer Handle-
OnScoreChangedEvent DispatcherInput: NewScore(Integer)-
OnTimeChangedEvent DispatcherInput: NewTime(Float)-
OnGameFinishedEvent DispatcherInput: bCleared(Boolean)-

Making TargetCoins a variable means you can tune the difficulty with a single number if 20 turns out to be too many. Not hardcoding magic numbers is a habit that pays you back later.

The three dispatchers exist so GameState never has to know about the UI. GameState just shouts "the score changed" without caring who's listening. The HUD comes and listens on its own (see Event-Driven Design with Event Dispatchers).

Run the Countdown

Start a one-second looping timer from Event BeginPlay.

Node graph starting a one-second looping countdown with Set Timer by Event from Event BeginPlay, decrementing TimeRemaining in the Tick1Second custom event, and calling Finish Game when it hits zero
BP_CoinGameState (Event Graph)

Event BeginPlay
  → Set Timer by Event (Time = 1.0, Looping = true)
       Event pin → connect to Custom Event "Tick1Second"
       Return Value → Set CountdownHandle          // Save it so we can stop the timer later

Custom Event: Tick1Second
  → Set TimeRemaining (TimeRemaining - 1.0)
  → Call OnTimeChanged (NewTime = TimeRemaining)
  → Branch (TimeRemaining <= 0.0)
       True → Finish Game (bCleared = false)

Always save the return value (the Timer Handle) of Set Timer by Event into a variable. Without it, the countdown keeps running after the game ends and Finish Game gets called over and over behind the result screen.

Count the Coins

Create two functions. First, Add Coin (input: Amount(Integer)).

Function: Add Coin (Amount: Integer)
  → Branch (bFinished == true) → True: Return    // Stop counting after the game ends
  → Set Score (Score + Amount)
  → Call OnScoreChanged (NewScore = Score)
  → Branch (Score >= TargetCoins)
       True → Finish Game (bCleared = true)

Then Finish Game (input: bCleared(Boolean)).

Function: Finish Game (bCleared: Boolean)
  → Branch (bFinished == true) → True: Return    // Prevent finishing twice
  → Set bFinished = true
  → Clear and Invalidate Timer by Handle (Handle = CountdownHandle)
  → Save High Score (covered below)
  → Call OnGameFinished (bCleared = bCleared)

Both functions check bFinished at the top and bail out. Ending logic must run exactly once. Ignore that and the moment the 20th coin and the time running out land on the same frame, you get two result screens.

Wire Up the HUD

Create a Widget Blueprint named WBP_HUD (right-click → User Interface → Widget Blueprint → User Widget).

In the Designer tab, place the following.

WidgetNamePlacement
TextText_ScoreTop left (anchor top left too)
TextText_TimeTop center (anchor top center too)
Vertical BoxPanel_ResultScreen center. The three below go inside it
TextText_ResultInside Panel_Result (shows CLEAR! / TIME UP)
TextText_BestScoreInside Panel_Result (shows the high score)
ButtonBtn_RetryInside Panel_Result (with a "RETRY" Text inside it)

Set Panel_Result's Visibility to Collapsed in the details panel. It stays hidden during play and appears when the game ends. Layout and anchoring are covered in Getting Started with UMG.

Receive Notifications from GameState

In the Graph tab, connect to the GameState's dispatchers from Event Construct.

Node graph where WBP_HUD's Event Construct runs Get Game State and Cast, then binds three events to OnScoreChanged, OnTimeChanged, and OnGameFinished, with each custom event calling Set Text on a Text Block
WBP_HUD (Graph)

Event Construct
  → Get Game State → Cast To BP_CoinGameState
  → Bind Event to OnScoreChanged   → Custom Event "UpdateScore" (NewScore)
  → Bind Event to OnTimeChanged    → Custom Event "UpdateTime" (NewTime)
  → Bind Event to OnGameFinished   → Custom Event "ShowResult" (bCleared)
  → UpdateScore (NewScore = GameState's Score)   // Call once ourselves for the initial display
  → UpdateTime (NewTime = GameState's TimeRemaining)

Custom Event: UpdateScore (NewScore)
  → Text_Score → Set Text (Format Text "COINS {0} / {1}" with NewScore and TargetCoins)

Custom Event: UpdateTime (NewTime)
  → Text_Time → Set Text (Format Text "TIME {0}" with Truncate(NewTime))

Drag a wire from the red Event pin on the left side of a Bind Event to ... node and pick Add Custom Event ..., and UE creates a Custom Event with matching parameter types and count automatically. Build it by hand and you'll get the parameters wrong and fail to connect, so learn this shortcut.

Truncate is the node that drops the decimals and returns an integer (search for Truncate). It gives you TIME 42 instead of TIME 42.0.

The important bit is those last two lines, where we call the initial display once ourselves. Dispatchers only fire "when something changes," so without this you'd see placeholder text like COINS 0 / 0 for the first second.

Not using Binding to update the Text is deliberate. Bindings are evaluated every frame, which is overkill for a display that changes once per second (see UMG Focus Management and Invalidation Box Optimization).

Put It on Screen

Create the HUD and add it to the screen from BP_ThirdPersonGameMode's Event BeginPlay.

BP_ThirdPersonGameMode (Event Graph)

Event BeginPlay
  → Create Widget (Class = WBP_HUD, Owning Player = Get Player Controller (0))
  → Add to Viewport

At this point, pressing Play should show the coin count in the top left and the remaining seconds in the top center, with the number rising each time you grab a coin. Play once and confirm this much works. Seeing it run before you move on makes it far easier to find what broke later.

Clear and Game Over

Now build the contents of the ShowResult event.

Custom Event: ShowResult (bCleared)
  → Text_Result → Set Text (bCleared ? "CLEAR!" : "TIME UP")   // Branch with a Select node
  → Text_BestScore → Set Text (Format Text "BEST {0}" with GameState's BestScore)
  → Panel_Result → Set Visibility (Visible)
  → Get Owning Player → Set Show Mouse Cursor (true)
  → Set Input Mode UI Only (Player Controller = Owning Player, In Widget to Focus = Panel_Result)
  → Set Game Paused (true)

We'll write the high score into GameState's BestScore in the next section. ShowResult just reads it. By the time OnGameFinished fires, the save work on the GameState side is already done, so you don't need to worry about ordering.

We use Set Input Mode UI Only because on the result screen all you do is click a button with the mouse and gameplay input isn't needed. Conversely, in a situation like a pause menu where you want to close it with the same key, this mode would leave you unable to close it. Use Game And UI there instead (see Implementing a Pause Menu Correctly).

For the retry button, select Btn_Retry in the Designer and press the + next to On Clicked in the details panel to add it to the graph.

On Clicked (Btn_Retry)
  → Set Game Paused (false)      // Unpause before transitioning
  → Open Level (by Name) (Level Name = Get Current Level Name)

Put unpausing before Open Level. In most cases the level gets rebuilt anyway so it would work without it, but switching screens while leaving a pause state active is a breeding ground for bugs. Undo what you stopped, right where you stopped it.

Get Current Level Name has Remove Prefix String enabled by default, so it strips the UEDPIE_ prefix added during Play In Editor (PIE) and returns a clean name you can pass straight to Open Level. For level transitions themselves, see Getting Started with Level Transitions.

Save the High Score

Reloading the level with Open Level rebuilds the GameState along with everything else, so the high score would be lost. We need to write it to disk.

Blueprint Class → pick Save Game as the parent class and create BP_CoinSave. Give it exactly one variable.

VariableTypeDefault
BestScoreInteger0

Add one variable named SaveObject of type BP_CoinSave to BP_CoinGameState, then create the Save High Score function and call it from Finish Game.

Completed node graph where the Does Save Game Exist Branch splits into a Load Game from Slot path and a Create Save Game Object path, both merging into Set SaveObject before Save Game to Slot
Function: Save High Score

  → Branch (Does Save Game Exist (Slot Name = "CoinHighScore", User Index = 0))
       True  → Load Game from Slot ("CoinHighScore", 0)
             → Cast To BP_CoinSave → Set SaveObject
       False → Create Save Game Object (Class = BP_CoinSave)
             → Set SaveObject
  → (merged) Branch (Score > SaveObject.BestScore)
       True → SaveObject.BestScore = Score
            → Save Game to Slot (Save Game Object = SaveObject, "CoinHighScore", 0)
  → Set BestScore (SaveObject.BestScore)     // Copy into GameState for display

Routing through a SaveObject variable is the key here. In Blueprint, objects coming out of two branches of a Branch aren't treated as one value just because you merged the execution pins. Store them in the same variable on both branches, then use that variable downstream. Skip this and you'll be stuck on "the pin won't connect after the merge."

The load path needs Cast To BP_CoinSave, because Load Game from Slot returns the base Save Game type and you can't read BestScore off it directly (Create Save Game Object already specifies the class, so no cast is needed there). Saving is covered in depth in Implementing a Save/Load System.

That last line copies the value into GameState's BestScore, so the result screen only has to display it. Just seeing "I got better than last time" is reason enough to play again.

Sponsored

Hands-On: Three Additions That Improve Game Feel in 30 Minutes

At this point the one-sentence spec is satisfied. The game is playable. Whether it feels good to play is a separate question.

From here, we'll make three additions that change nothing about the spec and improve only the feel. Action game, puzzle game, racing game: this is the layer that ends up mattering in all of them.

Here's what you'll see. Particles of light burst the instant you touch a coin, and the pitch of the sound climbs a little with each pickup. When under 10 seconds remain, the timer at the top center starts flashing red. The numbers climb exactly the same way, but the sense of being pressed for time is unmistakable.

Comparison of a plain screen with no effects on the left and a screen with coin pickup Niagara particles, rising SFX pitch, and a red flashing timer on the right

Setup

ItemValue
TargetCoins on BP_CoinGameState20
Default TimeRemaining on BP_CoinGameState60.0
Variable to add (BP_CoinGameState)ComboPitch (Float / default 1.0)
Effect to useOne Niagara System you make yourself (named NS_CoinPickup)
SFX to useAny short sound from Content/StarterContent/Audio/ in Starter Content

The one thing you have to prepare is the effect. The particles included in Starter Content are the older Cascade generation (P_Sparks and friends) and can't be passed to Spawn System at Location (those use Spawn Emitter at Location instead). Follow the steps in Getting Started with Niagara to make a single NS_CoinPickup that just bursts some particles. It takes five minutes.

Addition 1: Raise the Pickup Pitch Gradually

When the sound gets higher with each coin, you get a sense of building a streak. Add two lines to Add Coin in BP_CoinGameState.

In Function: Add Coin (Amount: Integer), right after Set Score
  → Set ComboPitch (Clamp(1.0 + Score * 0.03, 1.0, 1.6))

On the BP_Coin side, connect GameState's ComboPitch to the Pitch Multiplier pin on Play Sound at Location. That pin is hidden by default, so click the small downward arrow (Advanced Pin) at the bottom of the node to expand it. "That pin doesn't exist" is usually this.

The Clamp caps it at 1.6 so the 20th coin doesn't become shrill and unpleasant.

The sound plays before Add Coin, so the pitch rise starts from the next coin. The first one plays at the base pitch and climbs from there, which actually sounds more natural.

If you want to go deeper on the sound design itself, see Sound Cue vs. Sound Wave. You can also randomize pitch with a Sound Cue's Modulator node.

Addition 2: Burst Particles on Pickup

Assign your Niagara system to the Spawn System at Location node you left in BP_Coin's graph. Adding about Z + 50 to GetActorLocation makes the burst read as coming from the middle of the coin, which looks better.

When you want to build your own effects, head to Getting Started with Niagara. Here, dropping in one ready-made system is plenty.

Addition 3: Flash Red Under 10 Seconds

Add a branch to UpdateTime in WBP_HUD.

Node graph where the UpdateTime event uses a Branch to check for 10 seconds or less, plays a red flashing animation with Play Animation, and otherwise leaves the display white

First, in the Designer, create Anim_Warning from the Animations panel. Add Text_Time's Color and Opacity to the track and key white at 0 seconds, red at 0.25 seconds, and white at 0.5 seconds. Leave Loop unchecked in the details panel; we'll loop it from the caller.

Custom Event: UpdateTime (NewTime)
  → Text_Time → Set Text (Format Text "TIME {0}" with Truncate(NewTime))
  → Branch (NewTime <= 10.0 AND Is Animation Playing(Anim_Warning) == false)
       True → Play Animation (Anim_Warning, Num Loops To Play = 0)   // 0 means loop forever

Is Animation Playing is in the condition to prevent the animation from restarting from the beginning every second. When you start playback from an event that fires every second, always check whether it's already playing. That pattern applies well beyond flashing text.

Verify

Hit Play and run until you've collected all 20. The sound climbs a little with each pickup, particles burst, and under 10 seconds the timer at the top pulses red. The instant you grab the 20th coin, CLEAR! appears and time stops.

Here's how to narrow things down when it doesn't work.

  • The pitch doesn't rise → GameState's ComboPitch isn't reaching Pitch Multiplier. Expand the Advanced Pin and confirm you're going through Get Game State → Cast
  • The timer flashes constantly or keeps restarting → The Is Animation Playing condition is missing
  • Only the 20th coin has no sound or particles → In BP_Coin's graph, Add Coin comes before Spawn System and Play Sound. The 20th coin stops the game inside Add Coin, so reorder it to run the effects first

Two things to take away.

  • Feel lives outside the spec: none of the three changed the rules by a millimeter. It's still 20 coins in 60 seconds. And yet the experience is different. The order matters: finish the spec first, then spend time on this layer
  • Ride on the notifications you already have: all three were a few lines added to the existing Add Coin and UpdateTime. No new machinery. Once you start restructuring your design for the sake of effects, finishing gets further away (see Building Game Feel: Hit Stop and Screen Shake)

Bonus: Good to Know for Later

It isn't finished until it's shareable. Export an exe with Platforms → Windows → Package Project in the toolbar and zip the whole output folder to hand over. Before exporting, set Project Settings → Maps & Modes → Game Default Map to the level you want people to play. Leave it at the default and launching gives you an empty screen. Only after this can you say you made something. The steps and the pitfalls are collected in Getting Started with Packaging. Always play the exported build yourself once. "Works in the editor, doesn't work packaged" is common.

If you add something next, add another stage before you add enemies. Enemy AI sounds fun, but building it blows up the spec immediately. Making one more level and sending the player to it after clearing keeps that sense of completion going (see Getting Started with Level Transitions). When you want to carry the score across stages, that's Persisting Data with GameInstance.

Watch out for miscounting your 20 coins. Place only 19 and the game can never be cleared no matter how long you search, because TargetCoins is hand-typed as 20. If that worries you, you can set TargetCoins from the element count of Get All Actors of Class (BP_Coin) on Event BeginPlay. That node scans every Actor in the level and is expensive, so the rule is to call it exactly once, in BeginPlay.

Picking one person to play it speeds up finishing. Having a deadline in the form of "I'm handing this to someone" lets you cut off the urge to keep polishing. This is the practical version of "finish something small" from Learning Roadmaps and Goal Setting.

Summary

Lining up what we built, it was only four Blueprints.

BlueprintRole
BP_CoinGets counted and destroyed on touch
BP_CoinGameStateHolds score, remaining time, and end conditions, and announces changes
WBP_HUDListens for announcements, displays them, and shows the result
BP_CoinSaveKeeps the high score on disk

The relationship between those four is the skeleton of most games. It splits into something that holds the state (GameState), something that changes the state (Actor), and something that reflects the state (UI), connected by events in between. Add enemies or add stages and this shape doesn't change.

The most important part, though, was that first sentence. Deciding "collect 20 coins in 60 seconds to clear" up front is what got you here without getting lost along the way.

What single sentence describes the next game you'll build? If you can write it, you're already halfway done.

Further Reading