The UE Beginner's Capstone: Finish One Complete Coin-Collecting Game

Created: 2026-07-21Last updated: 2026-09-05

A capstone building a game where you collect 20 coins in 60 seconds with Blueprint. Connect coin pickup, score and time, HUD, results and retry, then add high-score saving and effects to make something playable on Windows.

Moving a character, destroying what you touch, putting numbers on screen. Even once you can build these individually, connecting them into a finished game is where people stall.

Here we build a game where you collect 20 coins in 60 seconds . First we make one loop of "collect → see results → play again", then add a high score and effects.

The subject is less about learning many new features and more about assigning roles to parts you have already touched and connecting them in order. It assumes you have used Blueprint variables and Branch. Branch is the node that splits execution into True when a condition holds and False when it does not.

The finished game: running a stage lined with coins, collecting while watching the HUD's coin count and remaining time

What You'll Learn

  • Separating coins, game state, and screen display as you build
  • How to convey score and timer to the HUD
  • Connecting clear, time up, retry, and record saving
  • Adding audio and effects and finishing it into a distributable form

Sponsored

Decide the finish condition and the parts you need

Start by deciding in one line what counts as finished.

Collect all 20 coins placed in one stage within a 60-second limit to clear. Running out of time is a game over.

We do not add enemies or multiple stages here. Deciding this condition first makes it easier to gather the parts in order.

Deriving stage, coins, score, timer, HUD, and result screen from the finish condition of 20 coins in 60 seconds
PartWho handles it here
Character, camera, movement inputUse the Third Person template
CoinBP_Coin . Reports being touched and disappears
Score, remaining time, end checkBP_CoinGameState
Numbers, results, retry buttonWBP_HUD
High score so farBP_CoinSave

Those four are the new assets. We also edit the template's GameMode to specify the GameState and display the HUD. Since we build it as a single-player game , we do not cover online synchronization.

Coins report pickup, the GameState manages score and time, and the HUD displays on notification. SaveGame keeps the record in a file

Prepare the stage and Blueprints

Create a Blueprint project with "Games → Third Person". Choose "None" where a Variant is offered. Starter Content is not required.

Save the template level as L_CoinStage . Keeping a floor you can move on plus a Player Start, and adding two or three ledges, is enough as a first stage. Play and confirm movement and jumping (see trying out shapes with Modeling Mode).

Create a Content/CoinGame folder and prepare these assets first. We build their contents in order below, so start by matching names and types.

NameType / parent class to create
BP_CoinBlueprint Class → Actor
BP_CoinGameStateBlueprint Class → All Classes, search for GameStateBase
WBP_HUDUser Interface → Widget Blueprint → User Widget
BP_CoinSaveBlueprint Class → All Classes, search for SaveGame

Open the template's BP_ThirdPersonGameMode and change "Class Defaults → Game State Class" to BP_CoinGameState. Keep character settings such as Default Pawn Class as they are.

From here, compile and save each Blueprint after building its logic and confirm the result with Play. If a referenced variable or function does not appear as a candidate, also check whether you compiled that Blueprint first.

Specify this GameMode in the level's "World Settings → GameMode Override". When that is None, "Project Settings → Maps & Modes → Default GameMode" is used. The point is confirming which GameMode is used in the level you have open .

The four assets to create first under Content/CoinGame, and how the GameMode and World Settings connect

Manage score and time in the GameState

The GameState is where you put what is currently happening in the game. Here it holds "how many collected", "how many seconds left", and "whether it ended". Coins request scoring and the HUD displays the numbers. Both reference the same place, so display and actual score are less likely to disagree.

Small games can put the score on the player instead. We use a GameState here as practice in separating parts' roles (see the Game Framework introduction).

Prepare values and change notifications

Create these variables in BP_CoinGameState and set defaults after compiling. Integer handles whole numbers and Boolean handles the two values true and false.

VariableTypeDefaultPurpose
ScoreInteger0Coins collected
TargetCoinsInteger20Coins required to clear
TimeRemainingInteger60Seconds remaining
bFinishedBooleanfalseWhether it has ended
BestScoreInteger0The best record shown on the result screen
CountdownHandleTimer HandleunsetIdentifying info for stopping this timer later

Next create these three from the "+" on "My Blueprint → Event Dispatchers".

DispatcherInput name and typeWhat it announces
OnScoreChangedNewScore : IntegerThe score changed to this value
OnTimeChangedNewTime : IntegerThe remaining time changed to this
OnGameFinishedbCleared : BooleanIt ended. true means cleared

An Event Dispatcher is a mechanism for calling registered logic when something changes. The GameState calls the notification and we later register on the HUD side that "when notified, rewrite the display". Creating one does not connect it to the HUD automatically.

Build the ending logic first

Create a function FinishGame from the "+" on "Functions" and add a Boolean input bCleared . A function packages a bundle of logic so you can call it by name.

The notation below is for reading execution order. Condition corresponds to a Branch's Condition and put in a value to a variable's Set node.

FinishGame (input: bCleared)
  Condition: bFinished
    True  → end here
    False → put true into bFinished
          → Clear and Invalidate Timer by Handle
             Handle = CountdownHandle
          → Call OnGameFinished (bCleared = the function's input)

Setting bFinished to true first means calling this function again later does not repeat the end notification. Before the timer exists, passing an unset Handle simply means there is nothing to stop.

FinishGame checking bFinished at the entry. True returns, and False sets bFinished to true and continues into stopping the timer and notifying

Count one coin

Create a function AddCoin and add a Boolean output Accepted . Accepted is the answer to "was this one counted". The coin side destroys itself after receiving that answer.

AddCoin (output: Accepted)
  Condition: bFinished
    True  → Return (Accepted = false)
    False → put "Score + 1" into Score
          → Call OnScoreChanged (NewScore = Score)
          → Condition: Score >= TargetCoins
               True  → FinishGame (bCleared = true)
                     → Return (Accepted = true)
               False → Return (Accepted = true)

A Return Node ends the function and returns the result to the caller. It returns false only when already finished, and true when counted regardless of whether that cleared the game. Placing a Return Node on each of the two true sides is fine.

Adding 1 to Get Score, saving into Set Score, and passing that value into OnScoreChanged's NewScore, then checking the clear condition

The diagram uses the value output on Set Score's right side to pass the saved value into the notification. After notifying, read the current value with Get Score and compare it with TargetCoins.

Decrease the remaining time every second

Create Custom Events StartCountdown and Tick1Second in the Event Graph. A Custom Event is an entry point for logic you name and call yourself.

Wire StartCountdown's white exec line into Set Timer by Event with Time 1.0 and Looping on. Connect Tick1Second's red delegate output into the timer node's red Event input. That registers "call this event every second". Tick1Second's white exec output feeds the subtraction logic below.

Pass Set Timer by Event's Return Value into Set CountdownHandle and wire the white exec line into the Set too. We save the Timer Handle so FinishGame can specify and stop the same timer.

Registering a one-second timer in StartCountdown, wiring Tick1Second's delegate into Event and the timer's return value into CountdownHandle
Tick1Second
  Condition: bFinished
    True  → end
    False → put Max(TimeRemaining - 1, 0) into TimeRemaining
          → Call OnTimeChanged (NewTime = TimeRemaining)
          → Condition: TimeRemaining <= 0
               True → FinishGame (bCleared = false)

Max picks the larger of two values, so the remaining time never goes negative. We start the timer later by calling StartCountdown after displaying the HUD. We do not start it from the GameState's BeginPlay.

Subtracting 1 from the remaining time, clamping to 0 or above with Max, saving into Set TimeRemaining, and passing that into OnTimeChanged's NewTime

The value output on the right of the Set node can pass the saved value onward. Save the subtraction result, notify, and read the current TimeRemaining with a Get for the final condition.

Sponsored

Build coins you collect by touching

Open BP_Coin and add these to Components.

ComponentSettings
Sphere CollisionDrag onto DefaultSceneRoot to make it the root. Sphere Radius 60
Static MeshA child of the Sphere. Assign Cylinder with Scale (1, 1, 0.1) , Rotation X 90, and Collision Presets NoCollision
Rotating MovementSet Rotation Rate's Z to 90

Cylinder can use UE's basic shapes. If you cannot find it in the mesh picker, turn on "Show Engine Content" and look for Engine/BasicShapes/Cylinder . The rotation speed is 90 degrees per second.

Set the Sphere's Collision Presets to Custom with Collision Enabled "Query Only", every channel response Ignore, and only Pawn set to Overlap . Turn Generate Overlap Events on too. Also confirm Generate Overlap Events is on for the player's Capsule Component.

Overlap is the mechanism reporting that two collision volumes overlapped. Not colliding with the thin coin visual and detecting pickup with a slightly larger sphere makes it easy to collect even brushing past from the side.

Count only once when the player touches

Select the Sphere and create the event from the "+" on "On Component Begin Overlap" in the Details panel.

  1. Place Cast To BP_ThirdPersonCharacter and connect the Overlap's Other Actor into Object. Wire the white exec line too.
  2. Continue from the success side into Cast To BP_CoinGameState , passing Get Game State 's Return Value into Object.
  3. Insert a DoOnce on the success side and call AddCoin from it. AddCoin's Target is the Cast's As BP Coin Game State .
  4. Connect AddCoin's Accepted into a Branch's Condition and call Destroy Actor (Target Self) from the True side. Do nothing on False.

A Cast confirms whether the retrieved object can be treated as the specified type. The first Cast checks the player and the second our own GameState. The failure sides do not proceed to scoring.

Two Casts confirming the Overlap's Other Actor as the player and Get Game State's return value as our own GameState

After passing both checks, continue into scoring and cleanup.

After the GameState Cast succeeds, calling AddCoin from DoOnce and destroying the coin only when Accepted is true

DoOnce lets only the first pass through. Even when several Overlaps reach one coin, it does not score repeatedly. We do not re-collect coins here, so leave Reset unconnected.

Place just one coin in the level and Play to touch it. The coin disappearing means Overlap → GameState scoring → the pickup-succeeded answer all connect. Stopping Play restores the coins in the level. Once this works, hold Alt and drag to make 20.

Put numbers on the HUD

A HUD is the score and remaining time you watch while playing. Place a Canvas Panel as the root in WBP_HUD's Designer and lay out these parts inside.

Score at the HUD's top-left, time at top center, and results and retry button in the center, with result parts grouped inside Panel_Result
Part nameTypePlacement and initial settings
Text_ScoreText BlockTop-left. Initial text COINS 0 / 20
Text_TimeText BlockTop center. Initial text TIME 60
Panel_ResultVertical BoxScreen center. Visibility Collapsed
Text_ResultText BlockChild of Panel_Result. For the result display
Text_BestScoreText BlockChild of Panel_Result. For the record display
Text_SaveStatusText BlockChild of Panel_Result. Initial text empty
Btn_RetryButtonChild of Panel_Result, containing a Text Block reading RETRY

Turn "Is Variable" on for the parts you specify from the Graph. Turn Btn_Retry's "Is Focusable" on too. Match the Canvas children's anchors to their placement; giving Panel_Result a screen-center anchor with Alignment (0.5, 0.5) and Position (0, 0) centers it. Adjust sizes so text is not cut off (see the UMG introduction).

Connect notifications to the display logic

Create a GameStateRef variable in WBP_HUD of type BP_CoinGameState Object Reference. A reference is what you hold to specify the same GameState later. It is not a Class Reference.

From Event Construct, connect in this order.

  1. Run Cast To BP_CoinGameState with Get Game State's Return Value as Object.
  2. On the success side, put As BP Coin Game State into Set GameStateRef .
  3. From GameStateRef, create Bind Event to OnScoreChanged , Bind Event to OnTimeChanged , and Bind Event to OnGameFinished and wire the white exec line in that order. Target is GameStateRef for all three.
  4. Drag from each Bind's red Event pin to create the corresponding Custom Event. Name them UpdateScore , UpdateTime , and ShowResult in order.

Bind registers "when this notification arrives, call this logic". Red lines register the destination and white lines represent execution order. It is not a matter of wiring the Bind's white output into a Custom Event's entry.

Wiring GameStateRef into the Bind's Target and UpdateTime's red delegate output into Event to register the remaining-time listener

Build UpdateScore's and UpdateTime's contents. Place a Text Block with a Get and create Set Text (Text) from it to specify the display Target.

EventSet Text's TargetFormat Text passed into In Text
UpdateScore (NewScore: Integer)Text_ScoreCOINS {Score} / {Target} . NewScore into Score, GameStateRef's TargetCoins into Target
UpdateTime (NewTime: Integer)Text_TimeTIME {Seconds} . NewTime into Seconds

Format Text fills {name} in the text with the specified values. Rather than passing raw numbers into a Text Block, turn them into the sentence you want to display first.

Putting the arriving NewTime into Format Text's Seconds in UpdateTime and passing the resulting TIME text into Text_Time's Set Text

After the third Bind in Construct, also place calls to UpdateScore and UpdateTime, passing GameStateRef's current Score and TimeRemaining. Waiting on notifications alone never delivers the initial values, so do the initial display yourself once. We build ShowResult's contents in the next section.

Display the HUD, then start the clock

Build the following into BP_ThirdPersonGameMode's Event BeginPlay. When the template has existing logic, append to it.

Create Widget (Class = WBP_HUD, Owning Player = Get Player Controller 0)
  → Add to Viewport (Target = Create Widget's Return Value)
  → Cast To BP_CoinGameState (Object = Get Game State's Return Value)
  → StartCountdown (Target = As BP Coin Game State)

We create this HUD once per play and show the end screen inside the same Widget. Play and confirm that "COINS 0 / 20" and "TIME 60" appear at the start, the time decreases every second, and each coin adds one point. The result screen does not appear yet at this stage.

Show results and allow another run

Add logic to WBP_HUD's ShowResult. A true bCleared means cleared and false means time up.

  1. With a Select conditioned on bCleared, choose the Text CLEAR! on true and TIME UP on false and Set Text it into Text_Result.
  2. Set Panel_Result to Visible with Set Visibility .
  3. Get Character Movement from Get Player Character (0) and, with it as Target, run Stop Movement Immediately then Disable Movement .
  4. With Get Owning Player as Target, set Show Mouse Cursor to true.
  5. Run Set Input Mode UI Only . Player Controller is Get Owning Player, In Widget to Focus is Btn_Retry, and Flush Input on where available.

Stopping movement and switching controls are separate jobs. The former stops the running character and the latter makes the button usable. We do not pause the whole game. That lets the coin light and sound we add later play after the result screen appears. Scoring is stopped by bFinished and the clock by stopping the timer.

Wire up the retry button

Create Btn_Retry's On Clicked and build it in this order.

Set Input Mode Game Only (Player Controller = Get Owning Player)
  → Set Show Mouse Cursor (Target = Get Owning Player, false)
  → Remove from Parent (Target = Self)
  → Open Level (by Name)
     Level Name = Get Current Level Name's Return Value

Turn Get Current Level Name's Remove Prefix String on. That strips the prefix added for editor play so it reopens with the saved level name.

Open Level rebuilds the character and GameState, so you restart with a movable character, 20 coins, and a 60-second clock. Retry logic includes returning input to the game before the screen change (see the Open Level introduction).

Stopping score, clock, and movement at the end and operating the UI, then returning input to the game and reopening the same level on retry

Try one pass here: wait 60 seconds without collecting → TIME UP → RETRY → collect 20 → CLEAR! If clearing is hard, lining coins up nearby just for the check is fine. Score and time staying put after the result appears, and retry returning to the initial state, means the game's basic shape is done.

Sponsored

Carry a high score into the next play

GameState values vanish when you reopen the level. To keep a best record for the next play, we use SaveGame . It is a mechanism for creating a container for the values you want to save and writing its contents to a file.

Add an Integer BestScore to BP_CoinSave with a default of 0. Add the following to BP_CoinGameState.

VariableTypeDefault
SaveObjectBP_CoinSave Object Referenceunset
SaveStatusTextempty

Prepare the save data

Create functions SaveHighScore and WriteBestScore in BP_CoinGameState. We split them so the branches for "read existing data" and "create it for the first time" converge on the same logic.

At the top of SaveHighScore, put Score into BestScore and clear SaveStatus. Then branch on Does Save Game Exist . Use Slot Name CoinHighScore and User Index 0 throughout. The slot name specifies which save file to read and write.

Save fileLogic to run
None (False)Create Save Game Object (Class BP_CoinSave) → put the Return Value into Set SaveObject → WriteBestScore
Exists (True)Load Game from Slot → Return Value into Cast To BP_CoinSave's Object → on success put As BP Coin Save into Set SaveObject → WriteBestScore
The load-side Cast failsPut "Could not read the record" into SaveStatus and end the function. Do not proceed to saving

Place a Set SaveObject and a WriteBestScore call on each of the two healthy branches. Coming from either, they use the save data in the same variable. Pass the same slot name and User Index into Load Game from Slot as well.

Compare this run with the previous record and write

In WriteBestScore, first check SaveObject with Is Valid . If invalid, put "Could not save the record" into SaveStatus and end. If valid, proceed in this order.

  1. Put Max(Score, SaveObject's BestScore) into the GameState's BestScore.
  2. Copy that value into the save data too with a Set BestScore targeting SaveObject.
  3. Call Save Game to Slot . Save Game Object is SaveObject, Slot Name is CoinHighScore, and User Index is 0.
  4. If Return Value is false, put "Could not save the record" into SaveStatus.

Despite the shared name BestScore, the GameState's is the value shown on screen and SaveObject's is the value written to file . Distinguish the Set node Targets. With a previous 12 and a current 8, it keeps 12; with a current 15, it updates to 15.

Comparing the previous record with this run's score and saving the larger. A previous 12 stays 12 against 8 and becomes 15 against 15

Once the record is decided, write it out to file.

Passing SaveObject into Save Game to Slot and checking the Return Value with a Branch, reporting on screen when saving failed

We still show the result screen when saving fails. In that case the displayed BEST may not survive to next time, so SaveStatus reports it. We do not overwrite an unreadable existing file with a new empty record.

Wire it into the ending and display

Insert SaveHighScore between "stop the timer" and "Call OnGameFinished" in FinishGame. In WBP_HUD's ShowResult, add the following before making Panel_Result visible.

  • Put GameStateRef's BestScore into Format Text as BEST {Score} and display it in Text_BestScore.
  • Display GameStateRef's SaveStatus in Text_SaveStatus.

We finish saving before notifying, so the HUD only reads the result. Trying three coins then time up, retry with one then time up, then five then time up lets you confirm BEST goes 3 → 3 → 5. Change TimeRemaining's default to 10 during the check and return it to 60 afterwards.

If you already saved a higher record, that value remaining is the correct behavior. To try the 3 → 3 → 5 sequence, match every read and write Slot Name to a separate practice name first.

Also stop Play and start again to confirm the record persists. To build out saving and loading in more depth, see the Save/Load introduction.

Hands-On: add audio, light, and a time warning

With a full loop working, make the feedback clearer while keeping the rules. We add a sound at the moment of touching, light scattering from the coin's position, and a blink at ten seconds remaining. Play after each addition and confirm the change.

The finished result adding light and sound at the pickup position and a red blinking TIME 8 to the coin-disappearing baseline

1. Play a pickup sound

Prepare a short sound effect as a Sound Wave or similar. Insert Play Sound at Location between "Accepted's True" and Destroy Actor in BP_Coin. Pass the prepared sound into Sound and Self's Get Actor Location into Location.

This node plays audio at the specified location. Destroying the coin right after does not become a setup where the sound is attached to the coin and destroyed with it.

Once comfortable, passing this value into Pitch Multiplier makes the sound rise slightly with each pickup. Float is the type handling decimals, and this calculation runs as decimals.

Clamp(1.0 + (GameState's Score - 1) × 0.03, 1.0, 1.6)

Read Score from the GameState obtained by the coin's Cast. Since this runs after AddCoin, the first coin has Score 1 and a multiplier of 1.0. The second is 1.03 and the twentieth is 1.57. Clamp brings the value into the specified range. If Pitch Multiplier is not visible, expand the extra pins with the arrow at the node's bottom.

2. Scatter light from the coin's position

Prepare a Niagara System that bursts once and ends, named NS_CoinPickup (see the Niagara introduction). Use a short pickup effect rather than something that loops forever.

Insert Spawn System at Location before BP_Coin's Destroy Actor. System Template is NS_CoinPickup, Location is Self's Get Actor Location, and Auto Activate and Auto Destroy are on.

We aligned the Sphere and mesh centers here, so it emits from that position. Niagara Systems and the older Cascade Particle Systems are different assets. What you pass into Spawn System at Location is the Niagara one.

Because we do not stop the whole game at the end, even the twentieth coin's light plays to completion. If effects stop the instant the result screen appears, check whether Set Game Paused crept into the logic you added.

3. Blink the time at ten seconds remaining

Create an animation named Anim_Warning in WBP_HUD's Designer. Place keys on Text_Time's Color and Opacity: white at 0 seconds, red at 0.25, and white at 0.5. Leave the text opacity at 1.

After UpdateTime's Set Text, place a Branch combining these conditions with AND.

  • NewTime is 10 or less.
  • NewTime is greater than 0.
  • Is Animation Playing (In Animation Anim_Warning) is false.

Call Play Animation on the True side with Target Self, In Animation Anim_Warning, and Num Loops to Play 0. That 0 specifies infinite looping. It is not re-called while already playing, so the blink continues without restarting every second.

Add Stop Animation (Anim_Warning) at the top of ShowResult. That stops the blink once the result screen appears.

Normal display at 11 seconds and above, a repeating white-red-white animation from 1 to 10 seconds, and stopping at the result display

Finally confirm, in order: waiting until ten seconds to see the blink, collecting a coin to see sound and light, the effect still appearing on the twentieth coin, and retry returning to TIME 60 without a warning.

Sponsored

Make it playable on Windows and confirm

Set "Project Settings → Maps & Modes → Game Default Map" to L_CoinStage and also list it among the maps to include in the package. Package for Windows in Development first and play from the exe in the output location. The procedure is covered in the packaging introduction.

Specifying Game Default Map and the included maps, packaging in Development, and playing from the exe
What to confirmExpected result
Launch the exeThe coin stage, COINS 0 / 20, and TIME 60 appear
Collect one coinExactly one point is added and only that coin disappears
Time up / collect 20TIME UP / CLEAR! appears and movement, scoring, and clock stop
Press RETRYYou can move and jump, restarting from 20 coins and 60 seconds
Close the app and reopenThe previous high score appears on the next result screen

If you switch the distribution build to Shipping, do the same confirmations on that output. What you hand over is not the exe alone but a ZIP of the whole output folder . Launching from your own extracted folder confirms it plays in the form you distribute.

Bonus: tune it while playing

With only 19 placed, you cannot clear

TargetCoins is the required count, not a variable that counts placements automatically. Check BP_Coin in the Outliner and see whether all 20 are there. When changing the count, match the placements and TargetCoins.

The next refinement can be placement alone

Even with the same 20 coins, lining them up straight, putting them on ledges, or creating a detour that collects several at once changes how it plays. First see whether you can reach them all in 60 seconds, then have someone else play and you will see difficulties the author cannot notice.

When you want to move on to additional stages, level transitions and carrying values with GameInstance are the next subjects.

Summary

Coins report pickup, the GameState manages score and time, and the HUD displays on notification. With that division of roles, we connected coin collecting through results and retry. Adding SaveGame keeps the previous record for the next play.

Even in a small game, getting to a playable-to-the-end form makes it concrete how parts connect. From here, change placement and the time limit and try the tuning that makes you want to play again.

Further Reading

Unreal Engine Notes in this section98