So far, every line of code we have written runs once. The computer reads it, does it, and moves on. That is fine when you want to do something a single time. But games are built on repetition. You want to draw a hundred stars, not one. You want to move every enemy on screen, not the first one. You want the same handful of instructions to run over and over, sixty times a second, for as long as the game is open.
Writing those instructions out by hand, once per star or once per enemy, would be madness. Instead, we use loops. A loop is a way of telling the computer "do this again, and again, and again, until I tell you to stop." It is one of the most powerful ideas in all of programming, and once it clicks, you will see loops everywhere.
You have actually been using a loop since Chapter 3 without us calling it one. Every time you typed invalidate() at the end of onDraw, you set up a cycle: draw, redraw, draw, redraw. In this chapter we will give that idea a proper name and learn how to build loops deliberately.
In this chapter, we will:
- Understand what a loop is and why games depend on them.
- Use the
whileloop to repeat code until a condition changes. - Use the
forloop to repeat code a set number of times. - Meet Kotlin's ranges (
..,until,downTo,step). - Put one loop inside another with nested loops.
- Use
breakandcontinueto steer a loop. - Learn why we have to keep loops lean to hit 60 frames per second.
- Try an optional AI exercise on loops.
Let's start with the simplest kind.
The Game Loop, Revisited
Before we write any new loops, it is worth naming the one we have already been relying on.
A game is, at its heart, a single loop that runs many times per second. Each pass through that loop does three jobs: it reads the player's input, it updates the state of the game (moving things, checking for collisions), and it draws the result on screen. Then it does the whole thing again. That cycle is called the game loop, and it is the beating heart of every game ever made.
In our projects so far, Android has been running this loop for us. We draw a frame in onDraw, call invalidate() to say "this is now out of date," and Android calls onDraw again. Draw, invalidate, draw, invalidate. It is a loop, even though we never wrote the word.
The loops in this chapter are a little different. They run to completion right now, all in one go, before the program moves on. We will use them to draw many things in a single frame and to step through ranges of numbers. Later, in Act 3, we will combine both ideas and build a real game loop by hand — an actual loop, running on its own thread, that we control completely. For now, let's learn the building blocks.
while Loops
The while loop is the simplest loop in Kotlin. It says: "as long as this condition is true, keep running this block of code." It checks the condition, runs the block, checks the condition again, runs the block again, and only stops when the condition finally becomes false.
The syntax looks a lot like an if statement from Chapter 4, and that is no accident — both are built on a condition that evaluates to true or false.
var countdown = 5
while (countdown > 0) {
println("T-minus $countdown")
countdown -= 1
}
println("Liftoff!")
In the preceding code, we start with countdown set to 5. The while loop checks its condition: is countdown greater than 0? It is, so the block runs. We print the message, then subtract one from countdown using the -= operator we met in Chapter 2. Now countdown is 4. The loop checks the condition again — still greater than zero — and runs again. This repeats with 3, 2, and 1. When countdown finally reaches 0, the condition is false, the loop stops, and the program moves on to print "Liftoff!".
Notice the $countdown inside the printed text. That is a small Kotlin convenience called string templates: a $ followed by a variable name drops that variable's current value straight into the string. So the loop prints "T-minus 5", "T-minus 4", and so on. It is a tidy way to build text out of variables, and we will use it often.
The Danger of the Infinite Loop
There is one thing you must respect about while loops. Look closely at that example. The only reason the loop ever stops is the line countdown -= 1. That line is what eventually makes the condition false.
Forget that line, and the condition stays true forever. countdown is always 5, always greater than zero, and the loop runs an infinite number of times. Your program freezes, locked inside a loop it can never escape:
var countdown = 5
while (countdown > 0) {
println("T-minus $countdown")
// Oops — we forgot to change countdown!
}
This is called an infinite loop, and it is one of the most common beginner mistakes. The fix is always the same: make sure that something inside the loop changes the condition, so the loop can eventually end.
On Android, an accidental infinite loop is especially nasty. If you write one inside onDraw, the screen will never finish drawing its frame, and your whole app will freeze and eventually crash with an "Application Not Responding" message. We will come back to this when we talk about performance at the end of the chapter.
for Loops and Ranges
A while loop is perfect when you do not know in advance how many times you need to repeat — you just keep going until some condition changes. But very often you do know. "Draw ten stars." "Check all five lives." "Step through positions zero to a hundred." For those situations, the for loop is cleaner.
A for loop in Kotlin walks through a sequence of values, running its block once for each one. The most common sequence is a range — a span of numbers from a start to an end.
for (i in 1..5) {
println("This is loop number $i")
}
In the preceding code, 1..5 is a range that means "the numbers 1, 2, 3, 4, 5." The for loop runs its block once for each of those numbers, and each time, the variable i holds the current one. So this prints "This is loop number 1", then "...2", all the way up to "...5", and then stops. We did not have to create i, increase it, or check it ourselves — the for loop handles all of that for us. That is exactly why it is safer than a while loop for counting: there is no condition for you to forget to update, so you cannot accidentally create an infinite loop.
The variable i is a traditional name for a loop counter (it stands for "index"), but you can call it anything. for (frame in 1..6) reads beautifully when you are stepping through six frames of animation.
Different Kinds of Range
The .. operator creates a range that includes both ends. 1..5 includes both 1 and 5. That is usually what you want, but Kotlin gives you a few other ways to build a range, and each is useful in its own situation.
The until keyword creates a range that excludes the top end:
for (i in 0 until 5) {
println(i) // Prints 0, 1, 2, 3, 4 — stops before 5
}
This comes up constantly once we reach collections in Chapter 10, because a list of five things is numbered 0, 1, 2, 3, 4 — the count starts at zero. 0 until 5 gives you exactly those five numbers.
The downTo keyword counts backward:
for (i in 5 downTo 1) {
println(i) // Prints 5, 4, 3, 2, 1
}
And step lets you skip values, moving by more than one each time:
for (i in 0..100 step 10) {
println(i) // Prints 0, 10, 20, 30 ... up to 100
}
You can even combine them. for (i in 100 downTo 0 step 20) counts down from 100 to 0 in jumps of twenty. Between .., until, downTo, and step, you can describe almost any sequence of numbers you will ever need in a game.
Nested Loops
Here is where loops start to feel like a superpower. You can put a loop inside another loop. The inner loop runs all the way through, from start to finish, every single time the outer loop takes one step. This is called nesting, and it is exactly how we draw grids, boards, and tile maps.
Imagine you want to print the coordinates of every square on a small board, three columns wide and three rows tall:
for (row in 0 until 3) {
for (col in 0 until 3) {
println("Square at column $col, row $row")
}
}
In the preceding code, the outer loop picks a row and holds it. Then the inner loop runs completely, stepping through all three columns for that one row. Only when the inner loop finishes does the outer loop move on to the next row, at which point the inner loop runs all the way through again.
The result is nine lines of output — three columns times three rows. The outer loop ran three times, and each of those times, the inner loop ran three times. That multiplication is the key thing to understand about nested loops: a 3-by-3 grid is nine squares, a 10-by-10 grid is a hundred, and a 100-by-100 grid is ten thousand. The numbers add up fast, which is wonderful when you want to fill a screen with detail, and dangerous when you are not paying attention to performance.
Nested loops are the single most important idea for the next chapter, where we will use exactly this pattern — an outer loop for rows, an inner loop for columns — to paint a whole grid of colored squares across the screen in just a few lines.
break and continue
Most of the time, a loop should run its full course. But occasionally you want to bail out early, or skip a single pass. Kotlin gives us two keywords for steering a loop from the inside.
break stops the loop immediately and jumps to whatever comes after it. Picture searching through enemies for the first one that is still alive — once you find it, there is no point checking the rest:
for (enemyHealth in 0..10) {
if (enemyHealth > 0) {
println("Found a living enemy with health $enemyHealth")
break // Stop looking — we found one
}
}
continue is gentler. It skips the rest of the current pass and jumps straight to the next one, without ending the loop:
for (i in 1..10) {
if (i % 2 == 0) {
continue // Skip even numbers
}
println(i) // Only prints the odd numbers: 1, 3, 5, 7, 9
}
In the preceding code, the % 2 == 0 check (using the modulo operator from Chapter 2) is true for every even number. When it is true, continue skips the println, so only the odd numbers make it through. You will not need break and continue constantly, but when the situation calls for one, nothing else reads as cleanly.
Loops and Performance: The 60 FPS Budget
There is a reason loops deserve a careful eye in game programming specifically, and it comes down to one number: sixty.
Most phone screens refresh sixty times per second. To make a game feel smooth, we want to draw a fresh frame in time for each of those refreshes. That gives us a budget of roughly one-sixtieth of a second — about sixteen milliseconds — to do everything: read input, update the game, and draw the whole screen. Blow that budget, and frames start arriving late. The game stutters. It feels sluggish and cheap.
Every loop you run inside onDraw spends some of that sixteen-millisecond budget. A loop drawing a few hundred squares is no problem at all. But remember how quickly nested loops multiply. A nested loop drawing a thousand-by-thousand grid is a million squares, every single frame, and there is no phone on earth that will draw a million squares in sixteen milliseconds. The game grinds to a halt.
Two habits will keep you out of trouble. The first is to be aware of how many times a loop actually runs, especially a nested one — multiply the numbers and ask whether that count is sensible to do sixty times a second. The second is to do as little as possible inside the loop. If you are calculating the same value on every single pass, and that value never changes, calculate it once before the loop instead. We will see a concrete example of exactly this in the next chapter.
None of this should make you afraid of loops. Loops are how games achieve everything at scale. It is simply worth knowing, from the very start, that inside onDraw you are working against a clock.
Summary
Loops are how we tell the computer to repeat itself, and games are built on repetition. You now know two kinds. The while loop runs as long as a condition stays true, which is perfect when you do not know the number of repetitions in advance — just remember to change the condition inside the loop, or you will be stuck in it forever. The for loop walks through a range of values a known number of times, and Kotlin's ranges (.., until, downTo, and step) let you describe almost any sequence of numbers exactly. You also learned to nest one loop inside another to cover a grid, to steer a loop with break and continue, and to respect the sixteen-millisecond frame budget that loops inside onDraw are quietly spending.
In the next chapter, we put nested loops straight to work. We will build the Pattern Drawer — a project that paints a whole grid of colored, shifting squares across the screen using just two loops, one inside the other. It is the clearest demonstration in the book of how a few lines of looping code can produce something far bigger than the code that made it.
AI Exercise (Optional)
If you would like to push your understanding of loops a little further, here is an exercise for your AI assistant. As always, skip it if AI is not your thing — you will miss nothing essential.
Open your AI chatbot and paste in this prompt:
"I am a beginner learning Kotlin. I have just learned about
whileloops,forloops, and ranges (..,until,downTo,step), but I have not learned about functions, lists, or arrays yet. Please write a short Kotlin example that prints a multiplication table from 1 to 5 using two nestedforloops, and then explain, step by step, the order in which the loops run. Use only the features I have listed."
Notice how much that prompt does at once: it tells the AI who you are, exactly which features you know, and — crucially — which features it must not use. Without that last constraint, an AI will happily reach for functions or lists you have not met yet, and hand you code you cannot read.
When the answer comes back, trace through the nested loops yourself before you trust the AI's explanation. Can you predict the output on paper? Does the AI's account of which loop runs first match what you worked out? If the explanation is fuzzy, ask it to walk through just the first three lines of output one at a time. Make the AI teach at your pace.