3.3 — Fire Rate: Cooldowns & Time
Last lesson your turret did not fire shots, it fired a stream — every frame, for as long as you stood in range. Today it learns to wait.
The turret should shoot at me — but not sixty times a second. "Shoot at me, sometimes" is the whole feature, and there is nothing in it yet that a computer can act on.
First, before any code: cut that into parts small enough that you could look each one up on its own.
- Notice the player is still inside the radiusThe distance test from 3.1, re-evaluated every frame: true for sixty frames in a row, which is what a condition is.Read: the range check in your own 3.1 script, and the
Vector3.Distance(a, b)row in the quick reference. - write this partRead: the
Time.timerow in the quick reference, and the deadline you wrote in 1.3. - Refuse to fire until enough time has passedOne condition attached to the range check with
&&: the clock, against that remembered moment plus the gap you want.Read: the Cooldown row of the pattern table in the quick reference, and the gate you wrote in 1.3. - Record the new moment the instant a shot leavesOne line, run only when the turret actually fires — the only thing that closes the gate again.Read: the Cooldown row in the pattern table (
act, set last = Time.time).
Three of the four parts are code you have already written. The one with no precedent in this course is the memory: a field that holds a moment rather than a count.
The test: could you look this part up on its own, in one search? "Fire less often" cannot be looked up. "Compare a timestamp against the current time" can.
Your turn
The table in the code half prices four ways to time this action. Which did you choose, and what does your choice cost you — in your own words, not the table's?
Today's memory holds a moment on the clock. If the turret had to charge for a second before each shot, what would that field have to hold instead — and which of the four approaches would you reach for?
- Hierarchy → the two Turret instances in the Overworld, then Inspector → Turret Controller: the two places today's knob will appear. Instance values override the prefab's, so the two turrets are allowed to disagree on purpose.
- Game view → press Play and stand inside one turret's radius for five seconds, then Console → count the lines that appear. Write the number down: it is the rate your frames are running at, not a decision anybody made.
-
Once the walkthrough is done — Inspector → Turret Controller → your cooldown
knob (it is there now that the field exists). Set one turret to
2and the other to0.5, then Game view → press Play, stand inside a radius, and time ten shots with a stopwatch. Then change the value while the game is running and watch the pacing change with it.
| Approach | What you store | The gate | What it costs you |
|---|---|---|---|
(a) A Time.time timestamp |
One float: the moment the last shot happened. | Time.time >= lastShot + cooldown |
One line of state, and one failure mode you will meet in the walkthrough. It remembers when the last shot happened, not how many are owed — a second press inside the window is discarded, not queued. |
| (b) A countdown timer | One float: seconds remaining until the next shot is allowed. | timer <= 0f, then timer = cooldown |
Something has to tick it down, every frame, including the frames where the turret is not firing. In exchange it reads as "seconds until I can shoot", which is what a designer wants to see. |
(c) InvokeRepeating |
Nothing at all — the engine holds the schedule. | InvokeRepeating(nameof(Fire), 0f, cooldown) |
Almost no code, and that is the trap: your method is called on cadence whether or not the player is in range, so the range check has to move inside it. Cancelling is by method name, so a rename breaks the cancel silently. |
| (d) A coroutine loop | A running coroutine handle. | while (true) { yield return new WaitForSeconds(t); } |
A StartCoroutine call, a stored handle to stop it, and a second mental
model of when your code runs. It is the only row that expresses "charge, then burst,
then rest" cleanly. |
The question this answers“how do I stop shooting too fast?”
You are editing Assets/Scripts/TurretController.cs. Your 3.1 and 3.2 turret does not change: it still finds the target, measures the distance and spawns a projectile at the muzzle. Today adds one knob, one field and one condition. Keep your own names and read the shape.
This build leaves 2 lines for you to write. Everything around them is given, so you always know the shape of what you are producing.
Step 1Declare the knob and the memory
Both lines go in the class body, outside every method — the placement rule from 1.3.
// TurretController.cs — class body, above Start(), with your other fields.
[SerializeField] private float cooldown = 1.5f;
private float lastAttackTime;- 2The knob: how many seconds must pass between shots.
[SerializeField]puts it in the Inspector, so a designer sets the pacing without opening your code.It has to be afloat. Anintrounds every Inspector value down to a whole second, so0.5quietly becomes0— back to a turret that fires every frame, from a type the Inspector never shows you. - 4The memory: one float holding the moment, on the game clock, when this turret last fired.
private, because it is the turret's own bookkeeping.Where this line lives is the memory. Move it inside the method that reads it and it is rebuilt from nothing every frame, holding the same empty value each time. That is the scope rule from 1.3: the same line in the wrong place is not a memory, it is a habit.
Step 2Seed the memory before the first frame
Once, before any frame has run.
// TurretController.cs — inside Start(), with your 3.1 target lookup.
lastAttackTime = -cooldown;- 2A first value, so the turret may fire the moment the player walks into range instead of serving a penalty for a shot it never took.Zero is a real moment on this clock. Left at its default, the field claims the last shot happened when the scene began, and the gate below would refuse to fire for the first
cooldownseconds with the player standing in front of it.
Step 3Close the gate in Update()
The range check stays as you wrote it in 3.1. It is just no longer the only thing deciding whether the turret fires.
// TurretController.cs — inside Update(), on the range check from 3.1.
if (distance <= range && ______________________)
{
Fire();
________________
}- Your turn2The gate. Keep the range half, add
&&, and add the second condition: fire only when enough time has passed since the last shot.The clock that only counts up is theTime.timerow in the quick reference; the last shot is the field from step 1; the gap is the knob. Put those three into one comparison — and ask whether==could ever be true.==is the wrong operator. Frames do not land on exact times, so the clock almost never equals your remembered moment plus the cooldown — it jumps past it.>=means at or past, which is what "enough time has passed" says. The&&is not decoration: drop the range half and the turret fires at nothing, on cadence, from wherever it stands. - 4Your firing method from 3.2 — the one that spawns a projectile at the muzzle. Unchanged, however you named it.
- Your turn6The line that closes the gate again. It runs only when a shot has actually left the barrel: write the current moment into the field that remembers the last one.The field remembers a moment, and the gate compared it against the clock you are standing on right now. What is that clock's current value?The most common bug in this lesson. Leave it out and the gate never closes: the field keeps its old value, the condition stays true on the next frame and every frame after, and the turret is back to firing at your frame rate — the bug you started with, rebuilt inside the fix. Stamping it inside
Fire()instead protects any other caller of that method; keep it here for now, where the gate and its consequence sit on the same three lines.
Check your work: the finished filetry every gap first — then open this to compare
Compare against your own file rather than copying it: your names, your range, your cooldown default and your 3.2 spawn are yours, and the shape is what has to match.
// ------------------------------------------------------------------
// REFERENCE FILE — Assets/Scripts/TurretController.cs
// Written to be read, not pasted. Build it with the walkthrough
// above, one step at a time, then open this and compare.
// If it is sitting in your project unchanged, that is the thing
// to go and do now — the walkthrough is still above you.
// ------------------------------------------------------------------
using UnityEngine;
public class TurretController : MonoBehaviour
{
[SerializeField] private float range = 6f; // your number from 3.1
[SerializeField] private float cooldown = 1.5f; // the new knob
[SerializeField] private Transform playerTransform; // however you acquired it in 3.1
[SerializeField] private GameObject projectilePrefab;
[SerializeField] private Transform spawnPoint;
private float lastAttackTime; // the memory: a MOMENT, not a count
void Start()
{
// ...the target lookup you chose in 3.1 stays here...
// One cooldown BEFORE the game started, so the first shot is legal
// the moment the player walks into range.
lastAttackTime = -cooldown;
}
void Update()
{
if (playerTransform == null)
{
return;
}
float distance = Vector3.Distance(transform.position, playerTransform.position);
// Both halves must be true: in range, AND enough time since the last shot.
if (distance <= range && Time.time >= lastAttackTime + cooldown)
{
Fire();
lastAttackTime = Time.time; // closes the gate again
}
}
private void Fire()
{
// ...the projectile spawn you wrote in 3.2, unchanged...
}
}- The knob. Which serialized float lets a designer set the gap between shots without recompiling, and what default will you ship for it?
- The memory. What does your row keep between frames — a moment, a duration, a handle, or nothing the engine is not already holding — and why can it not live inside the method that reads it?
- The gate and the reset. Which two conditions must both be true before the turret may fire, and which operator says "both"? What changes at the instant a shot actually leaves?
Choosing a Timing Model
- Trace your own 3.2 build: standing inside the radius for one second, how many projectile clones did your firing method create — and why did that number depend on the machine rather than on you?
- Why can't you call a "wait one second" function inside
Update()? Describe what the player would see on screen during that wait, and explain why a game engine refuses to let you block the frame. - Row (a) stores a moment; row (b) stores a duration. Which one can you print in the Console and have a player immediately understand, and which one is easier to compare against a designer's tuning value?
Checkpoint — before moving on, you must be able to:
- Break "the turret should not fire every frame" into its parts, and name the earlier lesson you lifted each one from
- Explain why your turret was firing once per frame, naming the line responsible
- Compare the four timing approaches, state what each one costs, and give a reason for your choice that is about this behaviour
- Explain why the reset line is mandatory, what bug returns without it, and construct a compound condition with
&&
Reference Docs
Need a deeper explanation? These pages cover the same concepts:
- Unit 3: The Turret — cooldowns and attack timing in depth
- Unity + C# Quick Reference —
Time.time, the Cooldown pattern row, and logical operators - How do I…? index — find this lesson again by the question you were asking
Next: 3.4 — Projectile Damage, Tuning & Playtest, where the bullets finally do something when they arrive.
Saved on this device only — the Dashboard tracks your progress.