For the last few projects, a complaint has been building. Every particle, every ring, every enemy has had its data smeared across a handful of parallel lists—one for x, one for y, one for velocity, one for life—and we have had to keep them all marching in perfect lockstep by hand. Add an object? Append to every list. Remove one? Delete from every list. Forget a single list and everything falls apart.
It works, but it is the wrong shape for the problem. All the data describing one particle clearly belongs together. And the things we do to a particle—move it, age it, draw it—clearly belong with that data too. What we have been missing is a way to say "here is what one particle is, and here is what one particle can do," all in one place.
That is exactly what object-oriented programming gives us. It is the most important idea in this book, and it is going to change how you write code from here on. Welcome to Act 3.
Code for this chapter: the complete code is in the
Chapter 16folder of the accompanying repository at github.com/EliteIntegrity/Learning-Kotlin-by-Building-Android-Games.
In this chapter, we will:
- Understand the problem that OOP solves.
- Define our own class—a blueprint that bundles data and behavior together.
- Create objects (instances) from a class.
- Give a class properties (its data) and methods (its actions).
- Use a constructor to set up an object when it is born.
- Hide a class's internals with encapsulation (
private). - Sketch the
Particleclass we will build the next project around. - Try an optional AI exercise on class design.
Let's meet the idea that ties the whole book together.
The Problem OOP Solves
You have actually been using objects since Chapter 1. Paint is an object—you make one, then ask it to do things (paint.color = ...). Canvas is an object you call methods on (canvas.drawCircle(...)). Every MutableList is an object with .add() and .size. You have been using other people's classes all along. Now you will learn to write your own.
Here is the core idea, with the analogy programmers have used for decades. A class is a blueprint. A blueprint for a house is not itself a house—it is a description of how to build one. From a single blueprint you can build many houses, each a real, separate thing, but all following the same plan. In code, a class is the blueprint, and the houses you build from it are called objects, or instances.
A good class bundles two things together: the data that describes one of these things, and the behavior—the actions you can perform on it. A Particle class would hold a particle's position, velocity, and life (the data), and know how to move itself and draw itself (the behavior). All of it, in one place, under one name. No more parallel lists.
Classes and Objects
Let's define the simplest possible class. We use the class keyword:
class Spaceship {
var x = 0f
var y = 0f
var health = 100
}
In the preceding code, we have defined a new type called Spaceship. Inside the braces are three properties—variables that belong to the class. Every spaceship we create will have its own x, its own y, and its own health. We met the word "property" back in Chapter 8: it is simply a variable declared at the class level rather than inside a function.
Defining a class creates a blueprint, not an object. To build an actual spaceship from it, we instantiate it—by writing its name followed by parentheses, exactly as if we were calling a function:
val playerShip = Spaceship()
val enemyShip = Spaceship()
Now we have two real, separate spaceships. This is the part that solves our problem. Each object carries its own copy of all the properties. We reach into an object's properties with the dot, the same dot you have used on paint and canvas all along:
playerShip.x = 500f
playerShip.health = 100
enemyShip.x = 200f
enemyShip.health = 30
Setting playerShip.x has no effect whatsoever on enemyShip.x. They are independent things built from the same blueprint. Imagine a hundred spaceships in a MutableList<Spaceship>—each one a tidy package holding all its own data. Compare that to a hundred entries spread across five parallel lists, and you can already feel the relief.
Methods: Functions That Belong to an Object
Data is only half the story. The real power of a class is that it can also hold functions—and a function that belongs to a class is called a method. A method can read and change the properties of the very object it is called on.
Let's give our spaceship some behavior:
class Spaceship {
var x = 0f
var y = 0f
var health = 100
fun takeDamage(amount: Int) {
health -= amount
}
fun isDestroyed(): Boolean {
return health <= 0
}
}
In the preceding code, takeDamage and isDestroyed are methods. They are just functions (the fun keyword from Chapter 8), but because they live inside the class, they can use the class's properties directly. Inside takeDamage, the word health means "this particular ship's health." We call methods with the dot, the same as properties:
val enemyShip = Spaceship()
enemyShip.health = 30
enemyShip.takeDamage(10) // enemyShip.health is now 20
enemyShip.takeDamage(25) // now -5
if (enemyShip.isDestroyed()) { // true
println("Enemy down!")
}
When you call enemyShip.takeDamage(10), the method runs on that ship, and health inside it refers to enemyShip's health. Call the same method on playerShip and it would touch playerShip's health instead. The object the method is called on is the object it works with. This is the heart of OOP: data and the behavior that acts on it, packaged together.
Occasionally a method needs to refer to its own object explicitly—for instance, to hand itself to another function. For that there is a special word, this, which always means "the object this method is running on." You will not need it often at first, and Kotlin lets you leave it out most of the time (writing health rather than this.health), but it is worth recognizing when you see it.
Constructors
Creating a Spaceship() and then setting its x, y, and health one line at a time is tedious. Most of the time we want to give an object its starting values the moment it is born. That is the job of a constructor.
In Kotlin, the most common constructor sits right in the class header, in parentheses after the class name. You have actually been looking at one since Chapter 1—class HelloCanvasView(context: Context). The (context: Context) is the constructor. Let's give Spaceship one:
class Spaceship(startX: Float, startY: Float) {
var x = startX
var y = startY
var health = 100
fun takeDamage(amount: Int) {
health -= amount
}
}
Now creating a spaceship requires us to supply a starting position, just like passing arguments to a function:
val playerShip = Spaceship(500f, 900f) // born at (500, 900)
val enemyShip = Spaceship(200f, 100f) // born at (200, 100)
The values we pass in fill the constructor parameters startX and startY, which we then use to set the properties x and y. The ship arrives fully formed.
Kotlin offers an even shorter form that is extremely common: you can declare the properties directly in the constructor by adding var or val in front of the parameters:
class Spaceship(var x: Float, var y: Float) {
var health = 100
// ...methods...
}
This does the same job in fewer words: writing var x: Float in the constructor both accepts x as a parameter and makes it a property of the class. This is the form you will see most often in real Kotlin, and the form we will use in our projects.
The init Block
Sometimes setting up an object needs more than just storing values—it needs to do something once, when the object is created. For that there is the init block, which you have also seen since Chapter 3, where we used it to set our paint's color. Any code inside init { } runs once, automatically, when an object is built:
class Spaceship(var x: Float, var y: Float) {
var health = 100
init {
println("A new spaceship was created at ($x, $y)")
}
}
The init block runs after the constructor parameters are available, so it can use x and y. Use it for any one-time setup an object needs at birth.
Encapsulation: public and private
There is one more core idea, and it is about protecting a class's data. Right now, any code anywhere can reach in and change a ship's health directly—enemyShip.health = 9999. Usually that is fine, but sometimes you want to guard a property so it can only be changed through your methods, where you can enforce the rules. That control is called encapsulation, and you achieve it with visibility modifiers.
By default, the properties and methods of a class are public—visible and usable from anywhere. If you mark something private, it can only be touched from inside the class itself:
class Spaceship(var x: Float, var y: Float) {
private var health = 100
fun takeDamage(amount: Int) {
health -= amount
}
fun isDestroyed(): Boolean {
return health <= 0
}
}
Now health is private. Code outside the class can no longer write enemyShip.health = 9999—the compiler forbids it. The only way to affect a ship's health is through takeDamage, which is the controlled, sensible path. The ship guards its own internals.
You have already used private without much comment—every project has declared private val paint = Paint(), keeping the paintbrush as a private detail of the view. The analogy is a car: you drive it through a public interface of pedals and a steering wheel, but the engine internals are sealed away where you cannot meddle with them by accident. Encapsulation lets your classes do the same—expose a clean set of controls, and hide the messy internals behind them. Use private for anything the outside world has no business touching directly.
Putting It Together: A Particle Class
Let's preview where this is heading. Remember the particle swarm from Chapter 11, with its five parallel lists? Here is the same particle, as a class:
class Particle(var x: Float, var y: Float, var velX: Float, var velY: Float) {
var life = 120
fun update() {
velY += 0.3f // gravity
x += velX
y += velY
life -= 1
}
fun isDead(): Boolean {
return life <= 0
}
}
Look at how everything that was scattered across five lists now lives in one tidy place. A particle's position, velocity, and life are its properties. Moving and aging it is a method, update. Checking whether it has expired is a method, isDead. One particle is now one object—a single, self-contained thing.
And a swarm of them becomes a single MutableList<Particle>, where updating the whole swarm is as simple as asking each particle to update itself. We will build exactly that in the next chapter, and the contrast with Chapter 11 will speak for itself.
Summary
Object-oriented programming bundles data and behavior into classes—blueprints from which we build objects. A class holds properties (its data) and methods (functions that act on that data, with the object they are called on being the one they affect). A constructor, written in the class header, gives an object its starting values at birth, and an init block runs any one-time setup. And encapsulation, through the private modifier, lets a class hide its internals and expose only a controlled set of methods, so its data cannot be corrupted by accident.
This is the foundation of everything in Act 3. In the next chapter we cash it in immediately: we will rebuild the particle swarm using a single Particle class and a single list, and you will see the parallel-list awkwardness vanish. After that we will learn how classes can build on one another through inheritance, which is where a whole zoo of different game objects becomes easy to manage.
AI Exercise (Optional)
Class design is one of the best things to think through with an AI, because deciding what belongs in a class is a real skill. Optional as ever.
Open your AI chatbot and paste in this prompt:
"I am learning Kotlin and have just learned the basics of classes: properties, methods, a constructor in the class header (including the
var x: Floatshorthand), the init block, and encapsulation with private. I have NOT learned inheritance yet. Design a single Kotlin class called Coin for a 2D game. It should have a position, a value (how many points it's worth), and a boolean for whether it has been collected. Give it a constructor that sets the position and value, a method to mark it collected, and a method that returns whether it can still be picked up. Make the 'collected' flag private so it can only be changed through the method. Briefly explain each choice."
The exercise asks the AI to make exactly the decisions you now understand: which fields are properties, which belong in the constructor, what the methods should be, and what to make private. Those are judgment calls, and forming your own opinion first is the point.
When the answer comes back, check it against what you would have written. Did the AI put the position and value in the constructor but leave collected to start as false inside the class? Did it actually make collected private and provide a method to change it? If it left collected public, push back: "doesn't that defeat the point of encapsulation?" Then try a follow-up: ask it to add a Coin to a MutableList<Coin> and loop through marking them collected—a preview of how a class and a collection work together, which is exactly where the next chapter goes.