Racing Blueprint, Build Your Own Racing Game
JVDesignStudio - https://jvdesignstudio.co.uk/workshops/racing-blueprint.html

Offline reference pack for this workshop.
This is a companion copy of the build steps, so you can follow along without
the website. Open the workshop online for the interactive screenshots, videos
and the copy buttons.

What you build: Racing Blueprint, Build Your Own Racing Game

--------------------------------------------------------------------
STEP-BY-STEP BUILD
--------------------------------------------------------------------

## STEP 01 ACTOR - Blueprint Actor Event Graph
- Connect Event BeginPlay to Print String. First wire in any Blueprint, confirms the engine is live.

## STEP 02 VARIABLES - Event BeginPlay',x:16,y:26,w:150,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func', title:'Print String',sub:'"Race Ready!"',x:205,y:26,w:148,lp:[{id:'l1',type:'exec',label:''}],rp:[{id:'r2',type:'exec',label:''}]}, ],[{f:'r1',t:'l1'}],[]), boxes:[{t:'box-c',tx:'The white arrow pins are exec wires, they control the order nodes run. BeginPlay fires once when the game starts. This first wire proves the whole graph is connected.'}], onApply(){G.phase='playing';initRace();sout('✅ Engine started! Track is live.');} }, {title:'Variables TopSpeed & Acceleration
- Connect BeginPlay ? Set TopSpeed ? Set Acceleration. Type your starting stats, these define how the car feels before any upgrades.

## STEP 03 TICK - Event BeginPlay',x:8,y:36,w:145,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'vset',title:'Set TopSpeed',x:174,y:14,w:120,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'float',label:'Value'}],rp:[{id:'r2',type:'exec',label:''}],vf:{hint:'Speed 100-500 (e.g. 300)',val:''}}, {id:'n3',type:'vset',title:'Set Acceleration',x:316,y:14,w:130,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'float',label:'Value'}],rp:[{id:'r3',type:'exec',label:''}],vf:{hint:'Accel 50-200 (e.g. 120)',val:''}}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3e'}],[ {id:'n2',ok:v=>v&&!isNaN(parseFloat(v))&&parseFloat(v)>0}, {id:'n3',ok:v=>v&&!isNaN(parseFloat(v))&&parseFloat(v)>0}, ]), boxes:[{t:'box-c',tx:'Float pins (green) hold decimal values like 300.5. TopSpeed is in cm/s in Unreal, so 300 = 300 cm/s. Acceleration in cm/s scales how quickly the car reaches top speed.'}], onApply(v){startCAR.topSpeed=(parseFloat(v['n2'])||300)/60;startCAR.accel=(parseFloat(v['n3'])||120)/1500;G.p.topSpeed=startCAR.topSpeed;G.p.accel=startCAR.accel;updateCarCard();sout('✅ TopSpeed='+v['n2']+' Accel='+v['n3']+
- Connect Event Tick ? Multiply (speed · delta) ? Add Movement Input. Delta Seconds keeps your car speed frame-rate independent.

## STEP 04 INPUT - Event Tick',sub:'~60x/sec',x:6,y:20,w:152,rp:[{id:'r1e',type:'exec',label:''},{id:'r1d',type:'float',label:'Delta Seconds'}]}, {id:'n2',type:'math',title:'× Multiply',x:182,y:14,w:125,lp:[{id:'l2a',type:'float',label:'Speed'},{id:'l2b',type:'float',label:'Delta'}],rp:[{id:'r2',type:'float',label:'Result'}]}, {id:'n3',type:'func',title:'Add Movement Input',x:330,y:6,w:158,lp:[{id:'l3e',type:'exec',label:''},{id:'l3s',type:'float',label:'Scale'}],rp:[{id:'r3',type:'exec',label:''}]}, ],[{f:'r1e',t:'l3e'},{f:'r1d',t:'l2b'},{f:'r2',t:'l3s'}],[]), boxes:[{t:'box-c',tx:'At 60fps Delta ≈ 0.016s. At 30fps ≈ 0.033s. Without multiplying by delta, your car drives faster on a high-end PC than on a laptop, unacceptable in a racing game where milliseconds matter.'}], onApply(){G.f_physics=true;initRace();sout('✅ Tick → Delta wired! Car physics active.');} }, {title:'Enhanced Input WASD Steering
- Connect BeginPlay ? Get Player Controller ? Get Enhanced Input Subsystem ? Add Mapping Context. Without this, no steering input works in UE5.

## STEP 05 STEER - Event BeginPlay',x:6,y:36,w:148,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Get Player Controller',x:178,y:18,w:165,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2p',type:'object',label:'Controller'}]}, {id:'n3',type:'func',title:'Get Enhanced Input\nSubsystem',x:6,y:116,w:182,lp:[{id:'l3p',type:'object',label:'Player Ctrl'}],rp:[{id:'r3',type:'object',label:'Subsystem'}]}, {id:'n4',type:'func',title:'Add Mapping Context',x:215,y:108,w:168,lp:[{id:'l4e',type:'exec',label:''},{id:'l4s',type:'object',label:'Target'},{id:'l4m',type:'object',label:'Mapping Context'}],rp:[{id:'r4',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2e',t:'l4e'},{f:'r2p',t:'l3p'},{f:'r3',t:'l4s'}],[]), boxes:[{t:'box-u',tx:'Content Browser ? Input ? Input Mapping Context. Name it IMC_Racing. Add IA_Steer (axis) and IA_Throttle (axis). Separate inputs for throttle and steering give more control than fused WASD.'}], onApply(){G.f_physics=true;sout('✅ Input mapping live! WASD drives the car.');} }, {title:'IA_Steer Lateral Input
- Connect IA_Steer (Triggered) ? Multiply by Handling ? Add Yaw Input. Higher handling = tighter corners.

## STEP 06 CAMERA - IA_Steer (Triggered)',sub:'Enhanced Input',x:4,y:20,w:172,rp:[{id:'r1e',type:'exec',label:''},{id:'r1v',type:'float',label:'Axis Value'}]}, {id:'n2',type:'math',title:'× Handling Scale',x:200,y:6,w:140,lp:[{id:'l2a',type:'float',label:'Axis'},{id:'l2h',type:'float',label:'Scale'}],rp:[{id:'r2',type:'float',label:'Yaw Delta'}]}, {id:'n3',type:'func',title:'Add Yaw Input',x:362,y:4,w:158,lp:[{id:'l3e',type:'exec',label:''},{id:'l3s',type:'float',label:'Val'}],rp:[{id:'r3',type:'exec',label:''}]}, ],[{f:'r1e',t:'l3e'},{f:'r1v',t:'l2a'},{f:'r2',t:'l3s'}],[]), boxes:[{t:'box-t',tx:'Low handling (0.5×) = realistic heavy car that understeers. High (2×) = kart-like instant response. Balance with top speed, a fast car with high handling is OP, which is fine for an arcade racer.'}], onApply(){sout('✅ Steering wired! Left/Right arrows turn the car.');} }, {title:'Camera Spring Arm Top-Down
- Connect BeginPlay ? Get Spring Arm Component ? Set Target Arm Length ? Set Relative Rotation to look straight down. Type the arm length.

## STEP 07 PHYSICS - Event BeginPlay',x:4,y:36,w:145,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Get Spring Arm',x:170,y:14,w:145,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2s',type:'object',label:'Arm Ref'}]}, {id:'n3',type:'func',title:'Set Target Arm Length',x:340,y:4,w:165,lp:[{id:'l3e',type:'exec',label:''},{id:'l3t',type:'object',label:'Target'},{id:'l3v',type:'float',label:'Length'}],rp:[{id:'r3',type:'exec',label:''}],vf:{hint:'Arm length 800-2000 (e.g. 1400)',val:''}}, ],[{f:'r1',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2s',t:'l3t'}],[{id:'n3',ok:v=>parseFloat(v)>=800&&parseFloat(v)<=2000}]), boxes:[{t:'box-c',tx:'A top-down racing camera uses a Spring Arm pitched to -90° (straight down). 800-1200 shows a tight view ideal for narrow tracks. 1400-2000 is better for wide tracks with AI rivals visible.'}], onApply(){sout('✅ Top-down camera wired! Spring arm set.');} }, {title:'Chaos Vehicle Component Setup
- Connect BeginPlay ? Get Vehicle Movement Component ? Set Max Engine Torque ? Set Mass. Type the engine torque.

## STEP 08 🚗 NAME - Event BeginPlay',x:4,y:34,w:145,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Get Vehicle Movement',x:170,y:12,w:165,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2v',type:'object',label:'Component'}]}, {id:'n3',type:'func',title:'Set Max Engine Torque',x:360,y:4,w:162,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'object',label:'Target'},{id:'l3t',type:'float',label:'Torque'}],rp:[{id:'r3',type:'exec',label:''}],vf:{hint:'Torque 200-600 (e.g. 400)',val:''}}, ],[{f:'r1',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2v',t:'l3v'}],[{id:'n3',ok:v=>parseFloat(v)>=200&&parseFloat(v)<=600}]), boxes:[{t:'box-c',tx:'Chaos Vehicle uses physical wheel simulation. Max Engine Torque controls how much force gets sent to the driven wheels. Combined with mass, this determines whether the car spins out or grips through corners.'}], onApply(){sout('✅ Chaos Vehicle physics configured!');} }, /* -- CAR DESIGNER -- */ {title:'Car Designer, Name
- Wire OnCarInit to Set Car Name. Type your car's name in the orange field. This shows above your car during the race.

## STEP 09 🚗 SPEED - On Car Init',x:8,y:34,w:135,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'car',title:'Set Car Name',sub:'Car Designer',x:168,y:12,w:155,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'string',label:'Name'}],rp:[{id:'r2',type:'exec',label:''}],vf:{hint:'Car name (e.g. Blaze GT)',val:''}}, {id:'n3',type:'func',title:'Apply to HUD',x:348,y:12,w:155,lp:[{id:'l3e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3e'}],[{id:'n2',ok:v=>v&&v.trim().length>=2}],true), boxes:[{t:'box-w',tx:'The name floats above the car during the race so other players and spectators can tell you apart from the AI. Keep it short, two words max looks best on narrow tracks.'}], onApply(v){startCAR.name=v['n2']||'Racer';updateCarCard();sout('✅ Car name: '+startCAR.name+'!');} }, {title:'Car Designer, Top Speed
- Wire OnCarInit to Set Max Speed. Type your top speed, higher speed means more distance between you and AI on straights, but corners become harder.

## STEP 10 🚗 ACCEL - On Car Init',x:8,y:34,w:135,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'car',title:'Set Max Speed',sub:'Car Designer',x:168,y:12,w:158,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'float',label:'cm/s'}],rp:[{id:'r2',type:'exec',label:''}],vf:{hint:'Speed 150-500 (e.g. 300)',val:''}}, {id:'n3',type:'func',title:'Apply Drivetrain',x:350,y:12,w:158,lp:[{id:'l3e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3e'}],[{id:'n2',ok:v=>parseFloat(v)>=150&&parseFloat(v)<=500}],true), boxes:[{t:'box-w',tx:'150 = slow kart. 300 = mid-range racer. 500 = formula car. The 3 AI rivals are set to 250-350 so staying competitive across the range is intentional.'}], onApply(v){startCAR.topSpeed=(parseFloat(v['n2'])||300)/60;G.p.topSpeed=startCAR.topSpeed;updateCarCard();sout('✅ Top speed: '+v['n2']+' cm/s!');} }, {title:'Car Designer, Acceleration
- Wire OnCarInit to Set Acceleration. Low accel = slow off the line, high accel = instant throttle response. Pairs with top speed for overall feel.

## STEP 11 🚗 HANDLING - On Car Init',x:8,y:34,w:135,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'car',title:'Set Acceleration',sub:'Car Designer',x:168,y:12,w:158,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'float',label:'cm/s'}],rp:[{id:'r2',type:'exec',label:''}],vf:{hint:'Accel 60-250 (e.g. 120)',val:''}}, {id:'n3',type:'func',title:'Init Engine Response',x:350,y:12,w:162,lp:[{id:'l3e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3e'}],[{id:'n2',ok:v=>parseFloat(v)>=60&&parseFloat(v)<=250}],true), boxes:[{t:'box-w',tx:'High speed + low accel = a car that feels sluggish from a standing start but dominates lap 2 once at pace. High speed + high accel = the fastest car but also the hardest to control through chicanes.'}], onApply(v){startCAR.accel=(parseFloat(v['n2'])||120)/1500;G.p.accel=startCAR.accel;updateCarCard();sout('? Acceleration: '+v['n2']+' cm/s²!');} }, {title:'Car Designer, Handling
- Wire OnCarInit to Set Handling Rating. Type a value 0.5-3.0. This scales how sharply the car rotates through corners.

## STEP 12 🚗 NITRO - On Car Init',x:8,y:34,w:135,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'car',title:'Set Handling',sub:'Car Designer',x:168,y:12,w:158,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'float',label:'0.5-3.0'}],rp:[{id:'r2',type:'exec',label:''}],vf:{hint:'Handling 0.5-3.0 (e.g. 1.5)',val:''}}, {id:'n3',type:'func',title:'Recalculate Yaw Rate',x:350,y:12,w:162,lp:[{id:'l3e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3e'}],[{id:'n2',ok:v=>parseFloat(v)>=0.5&&parseFloat(v)<=3.0}],true), boxes:[{t:'box-w',tx:'0.5 = understeer-heavy, hard to spin out. 1.5 = balanced. 3.0 = oversteer kart. Classic arcade racing balance: fast cars usually have lower handling to make high-speed corners a challenge.'}], onApply(v){startCAR.handling=(parseFloat(v['n2'])||1.5)*0.028;G.p.handling=startCAR.handling;updateCarCard();sout('✅ Handling: '+v['n2']+'/3.0 set!');} }, {title:'Car Designer, Nitro Canisters
- Wire OnCarInit to Set Nitro Slots. 1-4 canisters. More canisters means more overtake attempts each lap, balance vs top speed.

## STEP 13 🚗 COLOUR - On Car Init',x:8,y:34,w:135,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'car',title:'Set Nitro Slots',sub:'Car Designer',x:168,y:12,w:158,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'int',label:'Slots 1-4'}],rp:[{id:'r2',type:'exec',label:''}],vf:{hint:'Slots 1-4 (e.g. 2)',val:''}}, {id:'n3',type:'func',title:'Init Nitro Component',x:350,y:12,w:158,lp:[{id:'l3e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3e'}],[{id:'n2',ok:v=>parseInt(v)>=1&&parseInt(v)<=4}],true), boxes:[{t:'box-w',tx:'Nitro canisters refill from pickups on the track (the glowing N icons). 1 slot = conservative, save it for the last lap. 4 slots = aggressive overtake machine. Press SPACE to fire nitro.'}], onApply(v){startCAR.nitroSlots=parseInt(v['n2'])||2;G.p.nitroSlots=startCAR.nitroSlots;G.p.nitro=startCAR.nitroSlots;updateCarCard();G.f_nitro=true;sout('✅ Nitro slots: '+startCAR.nitroSlots+'. Collect N pickups, SPACE to boost!');} }, {title:'Car Designer, Colour
- Choose your car colour by connecting the matching colour node. Your car is immediately recoloured in the preview.

## STEP 14 NITRO - On Car Init',x:4,y:56,w:135,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'vget',title:'Get Colour Choice',x:155,y:30,w:148,rp:[{id:'r2c',type:'any',label:'Colour'},{id:'r2e',type:'exec',label:''}]}, {id:'n3',type:'car',title:'Set Car Material',sub:'Car Designer',x:324,y:14,w:158,lp:[{id:'l3e',type:'exec',label:''},{id:'l3c',type:'any',label:'Colour'}],rp:[{id:'r3',type:'exec',label:''}]}, ],[{f:'r1',t:'r2e'},{f:'r2c',t:'l3c'},{f:'r2e',t:'l3e'}],[]), boxes:[{t:'box-w',tx:'Colour helps the player instantly track their car in a pack of 4. High contrast vs AI colours matters most. Bright yellows and oranges read best at speed. Pale pastel colours can get lost against light track surfaces.'}], onApply(){ const cols=['#e8c41a','#f87171','#4ade80','#7ab8f5','#a78bfa','#fb923c','#2dd4bf']; startCAR.col=cols[Math.floor(Math.random()*cols.length)]; G.p.col=startCAR.col;updateCarCard();sout('✅ Car colour set: '+startCAR.col+'!'); } }, {title:'Nitro Blueprint, Input Event
- Connect IA_Nitro (Triggered) ? Branch (HasNitro?) ? True ? Activate Nitro ? Decrement Nitro Counter.

## STEP 15 NITRO SPD - IA_Nitro (Triggered)',sub:'Enhanced Input',x:4,y:34,w:170,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',x:196,y:14,w:100,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'HasNitro?'}],rp:[{id:'r2t',type:'exec',label:'True'},{id:'r2f',type:'exec',label:'False'}]}, {id:'n3',type:'func',title:'Activate Nitro',x:320,y:4,w:138,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'vset',title:'Decrement Nitro',x:320,y:62,w:145,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r3',t:'l4e'}],[]), boxes:[{t:'box-t',tx:'Branch prevents using nitro when slots are empty. The False pin does nothing, that\'s intentional. Trying to boost with no fuel should just be silent rather than showing an error message to the player.'}], onApply(){G.f_nitro=true;sout('✅ Nitro system live! SPACE to boost.');} }, {title:'Nitro Speed Multiplier
- Connect Activate Nitro ? Set Is Nitro Active ? Multiply TopSpeed by 1.5 ? Apply to Movement Component. Wire the exec chain.

## STEP 16 AI BASIC - Activate Nitro',x:4,y:34,w:138,lp:[{id:'l1e',type:'exec',label:''}],rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'vset',title:'Set IsNitroActive',x:164,y:14,w:148,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'bool',label:'True'}],rp:[{id:'r2',type:'exec',label:''}]}, {id:'n3',type:'math',title:'× Speed Boost',x:334,y:6,w:130,lp:[{id:'l3a',type:'float',label:'Speed'},{id:'l3b',type:'float',label:'1.5×'}],rp:[{id:'r3',type:'float',label:'Boosted'}]}, {id:'n4',type:'func',title:'Set Max Speed',x:334,y:68,w:138,lp:[{id:'l4e',type:'exec',label:''},{id:'l4v',type:'float',label:'Speed'}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l4e'},{f:'r3',t:'l4v'}],[]), boxes:[{t:'box-m',tx:'1.5× feels impactful without being unfair to AI. Reset back to base speed when the nitro timer expires (wire a Delay node + Set IsNitroActive False + reset max speed). '}], onApply(){sout('? Nitro speed boost 1.5× wired!');} }, {title:'AI Controller, Spline Following
- Connect BeginPlay ? Get AI Spline Path ? Set AI Move Target ? Start Following Spline. Wire the chain.

## STEP 17 AI SPEED - Event BeginPlay',x:4,y:36,w:145,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Get AI Spline Path',x:170,y:12,w:158,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2s',type:'object',label:'Spline'}]}, {id:'n3',type:'func',title:'Start Follow Spline',x:352,y:4,w:162,lp:[{id:'l3e',type:'exec',label:''},{id:'l3s',type:'object',label:'Spline Ref'}],rp:[{id:'r3',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2s',t:'l3s'}],[]), boxes:[{t:'box-c',tx:'Spline-following AI is the foundation of every racing game from Gran Turismo to Mario Kart. Place a Spline Actor along your racing line in the level editor, AI cars follow it automatically without pathfinding.'}], onApply(){G.f_ai=true;sout('✅ AI rivals activated! 3 opponents on the track.');} }, {title:'AI Speed Variance Per Rival
- Connect BeginPlay ? For Each Loop over AI array ? Multiply Random Float ? Set AI TopSpeed. Type the speed variance factor.

## STEP 18 PICKUP - Event BeginPlay',x:4,y:28,w:145,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'For Each AI Car',x:170,y:6,w:150,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2b',type:'exec',label:'Loop Body'},{id:'r2r',type:'actor',label:'Car Ref'}]}, {id:'n3',type:'math',title:'Random Float',sub:'In Range',x:170,y:86,w:145,rp:[{id:'r3',type:'float',label:'Speed'}],vf:{hint:'Variance e.g. 0.8-1.1',val:''}}, {id:'n4',type:'vset',title:'Set AI TopSpeed',x:340,y:72,w:152,lp:[{id:'l4e',type:'exec',label:''},{id:'l4v',type:'float',label:'Value'}]}, ],[{f:'r1',t:'l2e'},{f:'r2b',t:'l4e'},{f:'r3',t:'l4v'}],[{id:'n3',ok:v=>v&&v.includes('·')||parseFloat(v)>0}]), boxes:[{t:'box-t',tx:'Variance makes the race feel natural. 0.8-1.1 means some AI cars start a bit slow and catch up; others blast ahead and you have to chase. Identical AI speeds produce a boring single-file procession.'}], onApply(){sout('✅ AI speed variance applied, rivals have unique pace profiles!');} }, {title:'Overlap Event, Nitro Pickup
- On Sphere Overlap ? Cast To Player Car ? Branch (Is Player?) ? True ? Add Nitro Canister ? Hide Pickup Mesh.

## STEP 19 LAPS - On Component\nBegin Overlap',x:4,y:30,w:162,rp:[{id:'r1e',type:'exec',label:''},{id:'r1a',type:'actor',label:'Other Actor'}]}, {id:'n2',type:'cast',title:'Cast To BP_PlayerCar',x:188,y:10,w:162,lp:[{id:'l2e',type:'exec',label:''},{id:'l2o',type:'actor',label:'Object'}],rp:[{id:'r2s',type:'exec',label:'Success'},{id:'r2f',type:'exec',label:'Fail'}]}, {id:'n3',type:'func',title:'Add Nitro Canister',x:370,y:4,w:152,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Set Hidden In Game',x:370,y:62,w:150,lp:[{id:'l4e',type:'exec',label:''},{id:'l4b',type:'bool',label:'True'}]}, ],[{f:'r1e',t:'l2e'},{f:'r1a',t:'l2o'},{f:'r2s',t:'l3e'},{f:'r3',t:'l4e'}],[]), boxes:[{t:'box-c',tx:'Cast To BP_PlayerCar ensures only the player car triggers the pickup, AI cars ignore it. Hiding the mesh (not destroying the actor) lets you re-enable it after a few seconds for re-collection.'}], onApply(){G.f_nitro=true;sout('✅ Nitro pickup overlap wired! Collect the glowing N tokens.');} }, {title:'Lap Trigger Volume
- Connect OnOverlap ? Cast To Player ? Increment Lap Counter ? Branch (Lap >= 3?) ? True ? Trigger Race End.

## STEP 20 HUD POS - On Overlap\n(Start Line Vol.)',x:4,y:30,w:165,rp:[{id:'r1e',type:'exec',label:''},{id:'r1a',type:'actor',label:'Actor'}]}, {id:'n2',type:'cast',title:'Cast To BP_PlayerCar',x:192,y:10,w:162,lp:[{id:'l2e',type:'exec',label:''},{id:'l2o',type:'actor',label:'Object'}],rp:[{id:'r2s',type:'exec',label:'Success'}]}, {id:'n3',type:'vset',title:'Increment LapCount',x:376,y:4,w:148,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'branch',title:'Branch (Lap >= 3?)',x:376,y:70,w:148,lp:[{id:'l4e',type:'exec',label:''},{id:'l4c',type:'bool',label:'Cond'}],rp:[{id:'r4t',type:'exec',label:'True'},{id:'r4f',type:'exec',label:'False'}]}, ],[{f:'r1e',t:'l2e'},{f:'r1a',t:'l2o'},{f:'r2s',t:'l3e'},{f:'r3',t:'l4e'}],[]), boxes:[{t:'box-r',tx:'The start/finish line is a trigger volume (Box Collision set to Overlap). It fires every time any car passes through. Casting first filters for only the player car so AI laps don\'t accidentally trigger the end screen.'}], onApply(){G.f_laps=true;sout('✅ Lap counter live! 3 laps to win.');} }, {title:'HUD Widget, Race Position
- Connect Event Tick ? Get All Actors of Class (BP_Car) ? Sort By Progress ? Find Player Index ? Bind to WB_HUD Position Text.

## STEP 21 LAP TIMER - Event Tick',sub:'~60x/sec',x:4,y:28,w:140,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Get All Cars',sub:'Sort By Progress',x:166,y:6,w:152,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2a',type:'any',label:'Sorted Array'}]}, {id:'n3',type:'func',title:'Find Player Position',x:340,y:4,w:162,lp:[{id:'l3e',type:'exec',label:''},{id:'l3a',type:'any',label:'Array'}],rp:[{id:'r3e',type:'exec',label:''},{id:'r3p',type:'int',label:'Position'}]}, {id:'n4',type:'func',title:'Set Position Text',x:340,y:84,w:152,lp:[{id:'l4e',type:'exec',label:''},{id:'l4v',type:'int',label:'P Number'}]}, ],[{f:'r1',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2a',t:'l3a'},{f:'r3e',t:'l4e'},{f:'r3p',t:'l4v'}],[]), boxes:[{t:'box-c',tx:'Race position updates every tick. Sort by (laps completed × total waypoints) + current waypoint index. This gives a continuous progress score to rank all 4 cars. The HUD widget reads the player\'s rank from that sorted list.'}], onApply(){G.f_hud=true;sout('✅ HUD position tracking live!');} }, {title:'HUD Widget, Lap Timer
- Connect Event Tick ? Add Delta Seconds ? Bind to Lap Time Text. Wire exec then wire the delta float.

## STEP 22 CHECKPTS - Event Tick',x:4,y:28,w:140,rp:[{id:'r1e',type:'exec',label:''},{id:'r1d',type:'float',label:'Delta Seconds'}]}, {id:'n2',type:'math',title:'Add To Lap Time',x:166,y:6,w:145,lp:[{id:'l2e',type:'exec',label:''},{id:'l2d',type:'float',label:'Delta'}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2t',type:'float',label:'New Time'}]}, {id:'n3',type:'func',title:'Format Time String',x:334,y:4,w:152,lp:[{id:'l3e',type:'exec',label:''},{id:'l3t',type:'float',label:'Seconds'}],rp:[{id:'r3e',type:'exec',label:''},{id:'r3s',type:'string',label:'M:SS.ms'}]}, {id:'n4',type:'func',title:'Set Lap Time Text',x:334,y:80,w:152,lp:[{id:'l4e',type:'exec',label:''},{id:'l4s',type:'string',label:'String'}]}, ],[{f:'r1e',t:'l2e'},{f:'r1d',t:'l2d'},{f:'r2e',t:'l3e'},{f:'r2t',t:'l3t'},{f:'r3e',t:'l4e'},{f:'r3s',t:'l4s'}],[]), boxes:[{t:'box-t',tx:'Format to M:SS.ms so players see "1:24.3" not "84.352". Reset the lap time variable to 0 each time the start/finish trigger fires. Store it in a best lap variable if it\'s lower than the current best.'}], onApply(){G.f_hud=true;sout('✅ Lap timer wired and displaying!');} }, {title:'Checkpoint Ordering System
- Connect Checkpoint Overlap ? Get Next Expected Checkpoint ? Branch (IsCorrect?) ? True ? Mark Checkpoint Valid ? Advance Counter.

## STEP 23 COUNTDOWN - On Checkpoint Overlap',x:4,y:36,w:168,rp:[{id:'r1e',type:'exec',label:''},{id:'r1i',type:'int',label:'Checkpoint ID'}]}, {id:'n2',type:'branch',title:'Branch\n(IsCorrectOrder?)',x:195,y:10,w:155,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'Cond'}],rp:[{id:'r2t',type:'exec',label:'True'},{id:'r2f',type:'exec',label:'False'}]}, {id:'n3',type:'func',title:'Mark CP Valid',x:372,y:4,w:140,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'vset',title:'Advance Checkpoint',x:372,y:62,w:148,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1e',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r3',t:'l4e'}],[]), boxes:[{t:'box-r',tx:'Without ordered checkpoints, a player can park at the start/finish line and spam laps. The expected-checkpoint counter prevents this: you must hit every CP in sequence before the next lap registers.'}], onApply(){sout('✅ Checkpoint ordering system wired, no shortcut laps!');} }, {title:'Start Grid Countdown
- Connect BeginPlay ? Sequence ? Delay 1s × 3 ? On Complete ? Set RaceStarted True ? Enable Player Input.

## STEP 24 ? TURBO - Event BeginPlay',x:4,y:36,w:145,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Sequence',x:170,y:14,w:100,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2a',type:'exec',label:'Then 0'},{id:'r2b',type:'exec',label:'Then 1'},{id:'r2c',type:'exec',label:'Then 2'}]}, {id:'n3',type:'func',title:'Delay 1s ? Show 3',x:294,y:4,w:148,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Delay 1s ? Show 2',x:294,y:52,w:148,lp:[{id:'l4e',type:'exec',label:''}],rp:[{id:'r4',type:'exec',label:''}]}, {id:'n5',type:'func',title:'Delay 1s ? GO!',x:294,y:100,w:148,lp:[{id:'l5e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2a',t:'l3e'},{f:'r2b',t:'l4e'},{f:'r2c',t:'l5e'}],[]), boxes:[{t:'box-t',tx:'The Sequence node fires outputs in order without waiting. Delay nodes stall each branch independently. Disable player input before BeginPlay and re-enable it only after "GO!", otherwise the player can jump the start.'}], onApply(){sout('✅ Countdown 3-2-1-GO! wired. Race starts with 3 second countdown.');} }, {title:'Car Upgrade, Turbo Charger
- Choose the Turbo upgrade. Connect Apply Upgrade ? Get Current TopSpeed ? Multiply 1.25 ? Set Max Speed.

## STEP 25 ? SLICKS - On Upgrade Picked',x:6,y:34,w:148,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'vget',title:'Get TopSpeed',x:174,y:14,w:140,rp:[{id:'r2',type:'float',label:'Current'}]}, {id:'n3',type:'math',title:'× 1.25 Turbo',x:334,y:6,w:128,lp:[{id:'l3a',type:'float',label:'Speed'},{id:'l3b',type:'float',label:'1.25'}],rp:[{id:'r3',type:'float',label:'Boosted'}]}, {id:'n4',type:'vset',title:'Set Max Speed',x:334,y:68,w:138,lp:[{id:'l4e',type:'exec',label:''},{id:'l4v',type:'float',label:'Value'}]}, ],[{f:'r1',t:'l4e'},{f:'r2',t:'l3a'},{f:'r3',t:'l4v'}],[],true), boxes:[{t:'box-m',tx:'Turbo works best on cars with high base speed. It amplifies what\'s already there. Use it if you\'re already near the top of the AI speed range and need that edge on the final straight. Stacks badly with nitro.'}], onApply(){ carUpgrade1='turbo';startCAR.topSpeed*=1.25;G.p.topSpeed=startCAR.topSpeed;updateCarCard(); sout('? Turbo upgrade! Top speed ×1.25.'); } }, {title:'Car Upgrade, Slick Tyres
- Choose the Slick Tyres upgrade. Connect Apply Upgrade ? Get Handling ? Multiply 1.4 ? Set Handling Rating.

## STEP 26 ? AERO - On Upgrade Picked',x:6,y:34,w:148,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'vget',title:'Get Handling',x:174,y:14,w:140,rp:[{id:'r2',type:'float',label:'Current'}]}, {id:'n3',type:'math',title:'× 1.4 Grip',x:334,y:6,w:128,lp:[{id:'l3a',type:'float',label:'Handle'},{id:'l3b',type:'float',label:'1.4'}],rp:[{id:'r3',type:'float',label:'Sharper'}]}, {id:'n4',type:'vset',title:'Set Handling',x:334,y:68,w:138,lp:[{id:'l4e',type:'exec',label:''},{id:'l4v',type:'float',label:'Value'}]}, ],[{f:'r1',t:'l4e'},{f:'r2',t:'l3a'},{f:'r3',t:'l4v'}],[],true), boxes:[{t:'box-m',tx:'Slick tyres sacrifice braking distance for mid-corner grip, optimal on smooth circuits with fast sweepers. Combine with low-handling base stats for a great underdog build that dominates the twisty sections.'}], onApply(){ carUpgrade1='slicks';startCAR.handling*=1.4;G.p.handling=startCAR.handling;updateCarCard(); sout('? Slick tyres! Handling ×1.4.'); } }, {title:'Car Upgrade, Aero Spoiler
- Choose the Aero upgrade. Connect Apply Upgrade ? Multiply Acceleration by 1.3 ? Set Accel ? Multiply Handling by 1.15 ? Set Handling.

## STEP 27 DRAFT - On Upgrade Picked',x:4,y:36,w:148,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'math',title:'Accel × 1.3',x:174,y:14,w:120,lp:[{id:'l2a',type:'float',label:'Accel'},{id:'l2b',type:'float',label:'1.3'}],rp:[{id:'r2',type:'float',label:'New Accel'}]}, {id:'n3',type:'vset',title:'Set Accel',x:318,y:6,w:118,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'float',label:'Value'}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'math',title:'Handle × 1.15',x:174,y:86,w:128,lp:[{id:'l4a',type:'float',label:'Handle'},{id:'l4b',type:'float',label:'1.15'}],rp:[{id:'r4',type:'float',label:'New'}]}, {id:'n5',type:'vset',title:'Set Handling',x:318,y:78,w:118,lp:[{id:'l5e',type:'exec',label:''},{id:'l5v',type:'float',label:'Value'}]}, ],[{f:'r1',t:'l3e'},{f:'r2',t:'l3v'},{f:'r3',t:'l5e'},{f:'r4',t:'l5v'}],[],true), boxes:[{t:'box-m',tx:'Aero produces downforce, the car grips faster corners but also accelerates harder out of them. The dual-stat boost (accel + handling) makes it the most versatile upgrade for balanced stat builds.'}], onApply(){ carUpgrade1='aero';startCAR.accel*=1.3;startCAR.handling*=1.15; G.p.accel=startCAR.accel;G.p.handling=startCAR.handling;updateCarCard(); sout('? Aero spoiler! Accel ×1.3 and Handling ×1.15.'); } }, {title:'Nitro Draft Boost (Slipstream)
- Connect Event Tick ? Get Distance To Car Ahead ? Branch (< 200?) ? True ? Add Draft Boost ? Animate Slipstream FX.

## STEP 28 FINISH - Event Tick',x:4,y:28,w:130,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Get Distance\nTo Car Ahead',x:156,y:6,w:150,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2d',type:'float',label:'Distance'}]}, {id:'n3',type:'branch',title:'Branch (< 200?)',x:328,y:8,w:148,lp:[{id:'l3e',type:'exec',label:''},{id:'l3c',type:'bool',label:'Cond'}],rp:[{id:'r3t',type:'exec',label:'True'}]}, {id:'n4',type:'func',title:'Add Draft Boost',x:328,y:72,w:145,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r3t',t:'l4e'}],[]), boxes:[{t:'box-t',tx:'Slipstream (drafting) is the mechanic that keeps tight racing exciting, following within 200 units of a car in front gives a 10-15% speed boost. This rewards positioning and makes overtaking feel earned.'}], onApply(){sout('✅ Slipstream draft boost wired! Follow close for extra speed.');} }, {title:'Race End, Results Screen
- Connect Race Finished ? Stop All Input ? Get Final Positions ? Sort ? Show Results Widget ? Unlock Achievements.

## STEP 29 SAVE - Race Finished Event',x:4,y:34,w:160,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Disable Player Input',x:186,y:12,w:158,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2',type:'exec',label:''}]}, {id:'n3',type:'func',title:'Get Final Standings',x:366,y:4,w:155,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3e',type:'exec',label:''},{id:'r3r',type:'any',label:'Results'}]}, {id:'n4',type:'func',title:'Show Results Widget',x:366,y:70,w:155,lp:[{id:'l4e',type:'exec',label:''},{id:'l4r',type:'any',label:'Results'}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3e'},{f:'r3e',t:'l4e'},{f:'r3r',t:'l4r'}],[]), boxes:[{t:'box-r',tx:'Disabling input before showing results prevents phantom steering inputs from carrying over. Display: Position, Lap time, Best lap, and Nitro used. Show all 4 cars, players care how close it was even after finishing.'}], onApply(){sout('✅ Race end screen wired! Finish 3 laps to see results.');} }, {title:'Save System, Best Lap Time
- Connect Race Finished ? Get Current Best Lap ? Branch (< Saved Best?) ? True ? Save Game ? Update Leaderboard.

## STEP 30 FULL RACE - Race Finished',x:4,y:34,w:140,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Get Current Best Lap',x:166,y:12,w:158,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2t',type:'float',label:'Time'}]}, {id:'n3',type:'branch',title:'Branch\n(NewRecord?)',x:346,y:10,w:148,lp:[{id:'l3e',type:'exec',label:''},{id:'l3c',type:'bool',label:'Cond'}],rp:[{id:'r3t',type:'exec',label:'True'},{id:'r3f',type:'exec',label:'False'}]}, {id:'n4',type:'func',title:'Save Game + Leaderboard',x:346,y:86,w:165,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r3t',t:'l4e'}],[]), boxes:[{t:'box-c',tx:'UE5 Save Game objects persist across sessions. Store best lap time, total races, and wins. Show a record-breaking animation (gold flash, screen shake) when the player beats their best lap, it\'s one of the highest-feel moments in a racing game.'}], onApply(){sout('✅ Save system wired, best lap times persist between sessions!');} }, {title:'Full Race, Start Your Engine!
- Your racing game is complete. Enable the full race, 3 laps, 3 AI rivals, nitro pickups, and your custom car stats, then hit Fullscreen to race.
