At the end of the last chapter, our Fruit Catcher game had a problem we quietly stepped around. It could only ever track one apple. Why? Because each apple needs its own appleX, appleY, and appleSpeed. A second apple would mean apple2X, apple2Y, apple2Speed. A tenth would mean apple10X. And a hundred falling apples would mean three hundred variables and a level of misery no programmer should endure.
Games are full of "many of the same thing." Many enemies. Many bullets. Many particles. Many tiles in a level. We need a way to hold a whole group of values under a single name, and to work through them with a loop instead of by hand. That is exactly what collections are for, and in this chapter we meet the two most important ones: arrays and lists.
This is the chapter that unlocks scale. Once you can store a hundred things as easily as one, your games stop being toys and start being real.
In this chapter, we will:
- See why a pile of separate variables doesn't scale.
- Store a fixed group of values in an array.
- Access and change individual elements using an index.
- Store a flexible, growable group of values in a list.
- Add to and remove from a
MutableList. - Loop over a whole collection cleanly.
- Learn when to reach for an array and when to reach for a list.
- Try an optional AI exercise on collections.
Let's solve the too-many-variables problem properly.
The Problem: Too Many Variables
Imagine we want to store the high scores of five players. With what we knew before this chapter, we would write:
val score1 = 980
val score2 = 850
val score3 = 720
val score4 = 600
val score5 = 540
It works, but it is already awkward. How would you find the highest? You would have to compare all five by hand. How would you add a sixth player? You would have to write new code. And imagine this with five hundred players. The approach simply falls apart.
What we really want is to say "I have a group of scores — here they all are, under one name — and I can walk through them with a loop." That is a collection.
Arrays
An array is a fixed-size collection of values, all of the same type, stored under a single name. "Fixed-size" is the key phrase: when you create an array, you decide how many slots it has, and that number never changes.
Kotlin has special, efficient array types for numbers — IntArray for whole numbers and FloatArray for decimals — which we will use a lot, because games are full of numbers. Here is how we create one:
val scores = intArrayOf(980, 850, 720, 600, 540)
In the preceding code, intArrayOf builds an IntArray containing those five numbers. All five scores now live under one name, scores. That is already a huge improvement over five separate variables.
Accessing Elements by Index
Each value in an array has a position, called its index, and we reach a value using square brackets:
val firstScore = scores[0] // 980
val thirdScore = scores[2] // 720
Here is the single most important thing to learn about collections, and it trips up every beginner exactly once: indexing starts at zero. The first element is at index 0, the second at index 1, and so on. So in our five-element array, the valid indices are 0, 1, 2, 3, 4 — not 1 to 5. The last index is always one less than the size.
You met this idea already, back in Chapter 6, when we used for (i in 0 until 5) to get the numbers 0, 1, 2, 3, 4. That was no coincidence. 0 until size gives you exactly the valid indices of a collection, which is why you will see it constantly from here on.
If the array is not read-only, we can change an element by assigning to its slot:
scores[0] = 1000 // The first player just got a new high score
Be careful to stay in bounds. Asking for scores[5] or scores[99] in a five-element array is an error — Kotlin will crash your app with an ArrayIndexOutOfBoundsException, which is its way of saying "there is no slot there." The valid range is always 0 up to the size minus one.
The Size of an Array
Every array knows how many elements it holds, through its .size property:
println(scores.size) // 5
This is enormously useful, because it means we can loop over an array without knowing its size in advance:
for (i in 0 until scores.size) {
println("Score number $i is ${scores[i]}")
}
In the preceding code, 0 until scores.size produces every valid index — 0 to 4 — and inside the loop, scores[i] reaches the element at the current index. One small loop handles five scores, or five hundred, with no change to the code. That is the leverage we were missing.
Kotlin also has a general-purpose array type, Array<String>, for holding things that aren't numbers:
val playerNames = arrayOf("Alex", "Sam", "Jordan")
arrayOf works just like intArrayOf, but holds whatever type you put in it — here, Strings. You access and loop over it in exactly the same way.
Lists
Arrays are fast and simple, but their fixed size is a real limitation. In a game, you rarely know in advance how many bullets will be on screen or how many enemies are still alive. You need a collection that can grow and shrink as the game runs. That is a list.
Kotlin actually gives you two flavors of list, and the difference matters.
A List is read-only. You can look at its contents and loop over it, but you cannot add or remove elements after it is created:
val directions = listOf("North", "South", "East", "West")
A MutableList can be changed. You can add elements, remove them, and modify them at any time:
val enemies = mutableListOf<String>() // Starts empty
In the preceding code, mutableListOf<String>() creates an empty list ready to hold Strings. The <String> in the angle brackets tells Kotlin what type of thing the list will contain. (Those angle brackets are your first glimpse of something called generics, which simply means "a list of something." We will not dwell on it — just read MutableList<String> as "a changeable list of strings.")
The rule of thumb mirrors the one for val and var: prefer a read-only List unless you genuinely need to change the contents, in which case use a MutableList. A read-only list is safer, because nothing can modify it by accident.
Adding, Removing, and Reading
A MutableList comes with a set of built-in actions. Here are the ones you will use most:
val enemies = mutableListOf<String>()
enemies.add("Goblin") // The list now holds: Goblin
enemies.add("Orc") // Now: Goblin, Orc
enemies.add("Dragon") // Now: Goblin, Orc, Dragon
println(enemies.size) // 3
println(enemies[0]) // Goblin — same square-bracket indexing as arrays
enemies.removeAt(1) // Remove the element at index 1 (Orc)
// Now: Goblin, Dragon
In the preceding code, .add(...) appends a new element to the end of the list, growing it by one. .size reports how many elements there are right now — and unlike an array, that number changes as you add and remove. .removeAt(index) deletes the element at a given position and closes the gap. And reading an element uses the same [index] brackets as arrays, starting from zero.
This is the toolkit that finally lets us manage "many of something." Spawn a bullet? bullets.add(...). Bullet flies off screen? bullets.removeAt(...). The collection grows and shrinks to match what is actually happening in the game.
Looping Over a Collection
We have already looped using indices, and that works for both arrays and lists. But when you simply want to visit every element and don't care about its index, Kotlin offers a cleaner form of the for loop — the one we forward-referenced all the way back in Chapter 6:
val directions = listOf("North", "South", "East", "West")
for (direction in directions) {
println("You can go $direction")
}
In the preceding code, the loop hands you each element in turn, one per pass, in the variable direction. No indices, no .size, no square brackets. It reads almost like English: "for each direction in directions." This is the form to reach for whenever you just need to do something with every element.
There is also a handy .indices property, which gives you the range of valid indices for any collection:
for (i in directions.indices) {
println("Direction $i is ${directions[i]}")
}
directions.indices is simply a tidier way of writing 0 until directions.size. Use the plain for (item in collection) form when you only need the values, and the index form when you also need to know where each one sits — for instance, when you might need to change or remove it.
Arrays vs Lists: Which Should You Use?
Both store many values under one name, so which do you pick?
Reach for an array (IntArray, FloatArray, arrayOf) when you know exactly how many elements you need and that number will not change — a fixed set of high-score slots, the seven days of the week, a board of a known size. Arrays are a touch faster and simpler.
Reach for a list (mutableListOf) when the number of elements changes as the game runs — enemies that spawn and die, bullets that come and go, particles in an explosion. The ability to grow and shrink is worth everything in a real game, which is why lists are what we will use most often from here on.
When in doubt, a MutableList is rarely the wrong choice. Its flexibility covers the vast majority of game situations.
Summary
You have learned how to escape the trap of endless separate variables. An array holds a fixed number of same-typed values under one name, reached by an index that starts at zero. A list does the same but can grow and shrink: a read-only List for values that won't change, and a MutableList — with its .add(), .removeAt(), and .size — for collections that do. And you can sweep through any collection with a clean for (item in collection) loop, or use indices when position matters.
This is the key that unlocks scale, and we are going to turn it immediately. In the next chapter we build the Particle Swarm: a screen alive with dozens of independent particles, each with its own position, speed, and lifespan, all managed by lists and updated in a single loop. It is the moment our games stop handling one of something and start handling many.
AI Exercise (Optional)
Collections are a great topic to test an AI on, because there is often more than one reasonable way to solve the same problem. As always, skip this if you'd rather not use AI.
Open your AI chatbot and paste in this prompt:
"I am a beginner learning Kotlin. I have just learned about arrays (IntArray, arrayOf) and lists (listOf, mutableListOf), including .add(), .removeAt(), .size, indexing with
[ ], and looping withfor (item in collection). I have NOT learned about lambdas, classes of my own, maps, or sets yet. Using only those features, write a short program that starts with a mutableListOf five enemy names, adds two more, removes the one at index 0, and then prints every remaining enemy on its own line using a for loop. Explain why list indexing starts at 0 and what would happen if I tried to access an index equal to the list's size."
The prompt deliberately asks the AI to exercise add, remove, size, and looping all in one small program, and then to explain the zero-based indexing rule in its own words. That explanation is the real test — if the AI's account matches what you read in this chapter, you can be confident you've understood it.
When the answer comes back, trace the list's contents in your head after each line. After adding two and removing index 0, what is left, and in what order? Did the AI's final printout match your prediction? If the explanation of out-of-bounds access is vague, ask it: "show me the exact error Kotlin would throw and on which line." Make the AI be specific.