We’ve discussed object-oriented programming for game developers in general and the specific OOP principles of cohesion and coupling. Now let’s take a look at encapsulation and how it helps to keep code loosely coupled and more maintainable.
Note: Although this Quick Tip is explained using Java, you should be able to use the same techniques and concepts in almost any game development environment.
What Is Encapsulation?
Encapsulation is the principle of information hiding. That is, the implementation (the internal workings) of an object is hidden from the rest of the program.
A popular example you’ll hear for encapsulation is driving a car. Do you need to know exactly how every aspect of a car works (engine, carburettor, alternator, and so on)? No – you need to know how to use the steering wheel, brakes, accelerator, and so on.
Another example is searching for a value in an array. In Java, you can do the following:
int myArray[] = {1, 2, 3, 5, 7, 9, 11, 13}; Arrays.asList(myArray).contains(11);
The above code will return true
if the value 11
is in myArray
, otherwise it will return false
. How does the contains()
method work? Which searching technique does it use? Does it pre-sort the array before searching? The answer is it doesn’t matter because the exact implementation of the method is hidden.
Why Is Encapsulation Helpful?
Encapsulation helps to create code that is loosely coupled. Because the details are hidden, it reduces the ability of other objects to directly modify an object’s state and behavior.
It also greatly helps when you must change the data type of a variable. Lets say you decided to use a String
to keep track of time in “hh:mm:ss” format. After awhile, you come to realize that an int
representing seconds might be a better data type for time. Not only must you change the data type in the object, but also every time you referenced the object’s time in the entire program!
Instead, you can use what are known as getter and setter functions. Getters and setters are usually small functions that return and set a variable respectively. A getter function to get the time would look as follows:
public String getTime() { return time; }
The getter will return a String
value: the variable time
. Now when we want to change time
to an int, instead of changing all calls to the getter we can just change the getter function to change the int
data type into a String
data type.
public String getTime() { //split the time up into hours, minutes, and seconds int hours = time / 3600; int remainder = time % 3600; int minutes = remainder / 60; int seconds = remainder % 60; // //combine the result into a colon-separated string return hours + ":" + minutes + ":" + seconds; }The
%
operator is known as the modulus operator and returns the remainder of a division. So 10 % 3
would return 1
since 10 / 3 = 3
with a remainder of 1
.The getter function now returns a String
and none of the calls to the function have to change. The code is therefore more maintainable.
How to Apply This Principle
Asteroids
Lets go back to the example of a ship firing a bullet introduced in the coupling article. Recall that we defined a ship as follows:
/** * The Ship Class */ public class Ship { /** * Function – performs the behavior (task) of turning the Ship */ public void rotate() { // Code that turns the ship } /** * Function – performs the behavior (task) of moving the Ship */ public void move() { // Code that moves the ship } /** * Function – performs the behavior (task) of firing the Ship's gun */ public void fire() { // Code that makes the ship fire a bullet } }
To have the ship fire a bullet, all you would have to do is call ship.fire()
. How the code implements firing a bullet is not important, because all we care about is firing a bullet. This way, if you want to change the code to fire a laser blast instead, you just have to change the method fire()
and not every call to ship.fire()
.
This allows you to have the ability to change any aspect of how the ship works without having to change how the ship object is used everywhere else.
Tetris
Tetris has a few different ways to deal with clearing lines. Encapsulating the behavior of clearing a line will allow you to quickly change which method you use without having to rewrite each use of it. This will even allow you to be able to change the clear line method to create different modes of play in the game.
Pac-Man
There is one more feature of encapsulation that deals with hiding the access to an object. There are many times where you do not want external access to an object’s state or behavior, and so want to hide it from any other object. To do this, you can use access level modifiers which give or hide access to objects’ states and behaviors.
Access level modifiers define two access levels:
public
– any object can access at any time the variable or function.private
– only the object that contains the variable or function can access it.
(There is a third level – protected
– but we’ll cover that in a future post.)
Recall that a ghost had states of:
color
name
state
direction
speed
…and was defined as follows:
/** * The Ghost Class */ public class Ghost { /** * Function – moves the Ghost */ public void move() { // Code that moves the Ghost in the current direction } /** * Function - change Ghost direction */ public void changeDirection() { // Code that changes the Ghost's direction } /** * Function – change Ghost speed */ public void changeSpeed() { // Code that changes the Ghost's speed } /** * Function – change Ghost color */ public void changeColor() { // Code that changes the Ghost's color } /** * Function – change Ghost state */ public void changeState() { // Code that changes the Ghost's state // This function also will call the three functions of changeDirection(), changeSpeed(), and changeColor() } }
Since changeColor()
, changeSpeed()
, and changeDirection()
are helper functions and are not supposed to be accessed from anywhere else but inside the class, we can define them as private
functions. All the states of the ghost can also be declared private
since they shouldn’t be modified from outside the class either, and only accessed through getter and setter functions. This would change the class to be defined as follows:
/** * The Ghost Class */ public class Ghost { // Ghost states private String color; private String name; private Boolean isEatable; private Vector direction; private int speed; /** * Function – moves the Ghost */ public void move() { // Code that moves the ghost in the current direction } /** * Function - change Ghost direction */ private void changeDirection() { // Code that changes the Ghost's direction } /** * Function – change Ghost speed */ private void changeSpeed() { // Code that changes the Ghost's speed } /** * Function – change Ghost color */ private void changeColor() { // Code that changes the Ghost's color } /** * Function – change Ghost state */ public void changeState() { // Code that changes the Ghost's state // This function also will call the three functions of changeDirection, changeSpeed, and changeColor } /** * Getters and setters */ ... }
Conclusion
Encapsulation can help to create more maintainable code by helping to prevent the ripple effect of code changes. It also helps with creating loosely coupled code by reducing direct access to an object’s state and behavior.
In the next Quick Tip, we’ll discuss the principle of abstraction and how it can help to reduce code redundancy. Follow us on Twitter, Facebook, or Google+ to keep up to date with the latest posts.