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.
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
- Decide the finish condition and the parts you need
- Prepare the stage and Blueprints
- Manage score and time in the GameState
- Build coins you collect by touching
- Put numbers on the HUD
- Show results and allow another run
- Carry a high score into the next play
- Hands-On: add audio, light, and a time warning
- Make it playable on Windows and confirm
- Bonus: tune it while playing
- Summary
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.

| Part | Who handles it here |
|---|---|
| Character, camera, movement input | Use the Third Person template |
| Coin | BP_Coin . Reports being touched and disappears |
| Score, remaining time, end check | BP_CoinGameState |
| Numbers, results, retry button | WBP_HUD |
| High score so far | BP_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.

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.
| Name | Type / parent class to create |
|---|---|
BP_Coin | Blueprint Class → Actor |
BP_CoinGameState | Blueprint Class → All Classes, search for GameStateBase |
WBP_HUD | User Interface → Widget Blueprint → User Widget |
BP_CoinSave | Blueprint 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 .

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.
| Variable | Type | Default | Purpose |
|---|---|---|---|
Score | Integer | 0 | Coins collected |
TargetCoins | Integer | 20 | Coins required to clear |
TimeRemaining | Integer | 60 | Seconds remaining |
bFinished | Boolean | false | Whether it has ended |
BestScore | Integer | 0 | The best record shown on the result screen |
CountdownHandle | Timer Handle | unset | Identifying info for stopping this timer later |
Next create these three from the "+" on "My Blueprint → Event Dispatchers".
| Dispatcher | Input name and type | What it announces |
|---|---|---|
OnScoreChanged | NewScore : Integer | The score changed to this value |
OnTimeChanged | NewTime : Integer | The remaining time changed to this |
OnGameFinished | bCleared : Boolean | It 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.

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.

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.

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.

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.
Build coins you collect by touching
Open BP_Coin and add these to Components.
| Component | Settings |
|---|---|
| Sphere Collision | Drag onto DefaultSceneRoot to make it the root. Sphere Radius 60 |
| Static Mesh | A child of the Sphere. Assign Cylinder with Scale (1, 1, 0.1) , Rotation X 90, and Collision Presets NoCollision |
| Rotating Movement | Set 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.
- Place
Cast To BP_ThirdPersonCharacterand connect the Overlap's Other Actor into Object. Wire the white exec line too. - Continue from the success side into
Cast To BP_CoinGameState, passingGet Game State's Return Value into Object. - Insert a
DoOnceon the success side and callAddCoinfrom it. AddCoin's Target is the Cast'sAs BP Coin Game State. - 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.

After passing both checks, continue into scoring and cleanup.

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.

| Part name | Type | Placement and initial settings |
|---|---|---|
Text_Score | Text Block | Top-left. Initial text COINS 0 / 20 |
Text_Time | Text Block | Top center. Initial text TIME 60 |
Panel_Result | Vertical Box | Screen center. Visibility Collapsed |
Text_Result | Text Block | Child of Panel_Result. For the result display |
Text_BestScore | Text Block | Child of Panel_Result. For the record display |
Text_SaveStatus | Text Block | Child of Panel_Result. Initial text empty |
Btn_Retry | Button | Child 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.
- Run Cast To BP_CoinGameState with Get Game State's Return Value as Object.
- On the success side, put As BP Coin Game State into
Set GameStateRef. - From GameStateRef, create
Bind Event to OnScoreChanged,Bind Event to OnTimeChanged, andBind Event to OnGameFinishedand wire the white exec line in that order. Target is GameStateRef for all three. - Drag from each Bind's red Event pin to create the corresponding Custom Event. Name them
UpdateScore,UpdateTime, andShowResultin 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.

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.
| Event | Set Text's Target | Format Text passed into In Text |
|---|---|---|
| UpdateScore (NewScore: Integer) | Text_Score | COINS {Score} / {Target} . NewScore into Score, GameStateRef's TargetCoins into Target |
| UpdateTime (NewTime: Integer) | Text_Time | TIME {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.

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.
- With a
Selectconditioned on bCleared, choose the TextCLEAR!on true andTIME UPon false and Set Text it into Text_Result. - Set Panel_Result to Visible with
Set Visibility. - Get Character Movement from
Get Player Character (0)and, with it as Target, runStop Movement ImmediatelythenDisable Movement. - With
Get Owning Playeras Target, set Show Mouse Cursor to true. - 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).

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.
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.
| Variable | Type | Default |
|---|---|---|
SaveObject | BP_CoinSave Object Reference | unset |
SaveStatus | Text | empty |
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 file | Logic 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 fails | Put "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.
- Put
Max(Score, SaveObject's BestScore)into the GameState's BestScore. - Copy that value into the save data too with a
Set BestScoretargeting SaveObject. - Call
Save Game to Slot. Save Game Object is SaveObject, Slot Name is CoinHighScore, and User Index is 0. - 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.

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

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 TextasBEST {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.

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.

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.
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.

| What to confirm | Expected result |
|---|---|
| Launch the exe | The coin stage, COINS 0 / 20, and TIME 60 appear |
| Collect one coin | Exactly one point is added and only that coin disappears |
| Time up / collect 20 | TIME UP / CLEAR! appears and movement, scoring, and clock stop |
| Press RETRY | You can move and jump, restarting from 20 coins and 60 seconds |
| Close the app and reopen | The 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.