Suppose that we can create a window, load game images, and display them at arbitrary positions within the window (see previous post). How much more do we need to create an actual game?
The game state
First we need some variables that will contain the current state of the game. State is all information that we need to reconstruct the game at a given moment; all values that change during the game. What exactly that refers to, depends on the type of the game:
If it is a card game that multiple opponents play against each other, the state of the game might include the number of players, their names, how many points each player currently has, which player’s turn it is currently, cards that individual players hold in their hands, cards already placed on the table, and cards yet remaining in the deck.
If it is a platform game where the player jumps on various walls and platforms in a maze, collecting treasures and avoiding traps, the state of the game might include the positions of all objects in the game (including the player, the traps, and the yet uncollected treasures), current speed of the player (and of any other movable objects), and how many points the player already has.
If it is a puzzle game where the player needs to correctly arrange pieces of something, the state of the game might include the positions of all the pieces.
Shortly, state of the game is all the information you would need to save on disk so that you can turn off the program, and later turn it on, load the game and continue playing.
(Sometimes, there are parts of the game state that you do not need to save, because you can derive them otherwise. For example, if the game consists of walking in a maze, where the positions of the walls are predefined in the program, and the walls cannot be created nor destroyed, then we do not need to save and load the positions of the walls. Similarly we do not need to save cached values that were precomputed in order to increase the game speed if we can compute them again when needed.)
Then, we need to implement the following parts of the algorithm:
Display the game state on screen, whenever needed. This method can be called when something happened in the game, and we need to display the new situation. But it can also be called for reasons completely unrelated to the game, for example if some other window has moved over the game window, and now the operating system needs to refresh the game window.
At the beginning of the program, initialize the state. Put everything into its starting position, reset each player’s points to zero, create a new deck of cards and shuffle them, etc. Then, redraw the screen.
When something happens, update the state. Depending on the type of game, “something happens” can refer to pressing a key on the keyboard, moving or clicking the mouse, a tick on the internal game timer, receiving data from network server, etc. Updating the state can refer to moving, creating, and destroying things, adding or removing points, etc. Then, redraw the screen.
If you want to save and load the game, there are two more optional parts:
On saving the game, convert the game state into text or binary data, and save the data into a file on disk.
On loading the game, create a new game state based on text or binary data loaded from a selected file on disk; replace the current game state with this new state. Then, redraw the screen.
This is a simplified version that leaves out many technical details. The important thing to remember is to cleanly separate drawing the state from modifying the state. It makes the code less prone to mistakes, and easier to maintain. As you see, the screen can be redrawn on several different occassions, even for reasons unrelated to the game itself; if you don’t have a single command to paint the state, you will need to copy a longer and more complex code, and you are likely to make a mistake there.
Another good idea is to create a class that contains the entire game state. Then, initializing the state simply means creating a new instance of this class. Loading the game means creating a new instance of this class based on the saved file, and replacing the current instance with the new one… with the advantage that if loading the game fails (e.g. because the saved file was corrupt), the current instance remains unchanged, so after displaying an error message you can continue playing the original game.
If you imagine what the screen would look like in the middle of the game, and what information would you need to correctly position each image on the screen, that is approximately the state of the game. (You may also need to add variables for speeds of various objects, cooldown timers, and other information not apparent on a screenshot.)
Drawing the game state
To display images on the game screen, you need to create a subclass of Canvas
and redefine its “paint” method. The superclass implementation cleans the whole area of the canvas (replaces it with background color); if you redraw the entire area of the canvas, you do not need to call the superclass implementation.
The “paint” method is not called directly. To repaint the canvas, call its “repaint” method instead; this method will (among other things) create a new Graphics2D
object representing the surface of the canvas, and call the “update” method, which in turn calls the “paint” method.
The default implementation of the “update” method cleans the whole area of the canvas and then calls “paint”. (There are some technical and historical reasons to do so.) Given that the “paint” method is supposed to redraw the whole canvas anyway, you can override the “update” method to only call the “paint” method.
Putting this all together, the code for our custom canvas will be something like this:
var canvas = new Canvas() {
@Override
public void paint(Graphics g) {
... // draw the game state
}
@Override
public void update(Graphics g) {
paint(g);
}
};
At this moment, you can make a program that creates a new game state, and opens a new window containing a custom canvas that displays the (initial) game state.
Now we also need some interaction with the player.
Responding to key events
There are two ways to process keyboard actions. One option is to respond to physical actions of pressing a key, and releasing a key. Another option is to wait until a key (or a combination of keys) results in typing some characted. We usually want one or the other, not both.
To illustrate the difference, consider the usual way of writing capital “A” on the keyboard: first press the “Shift” key, then press the “A” key (at this moment the letter appears on the screen), and finally release both keys (technically, one of them gets released a few miliseconds sooner than the other).
From the perspective of writing, only one thing happened: “capital A was typed”.
From the perspective of pressing and releasing, four things happened: “Shift was pressed”, “key A was pressed”, “key A was released”, “Shift was released”.
The former perspective is more useful for writing text. The latter perspective is more useful for controlling games: pressing the Shift key could make the player character jump, and the time interval between pressing and releasing the Shift key could determine the height of the jump. From the perspective of typing, pressing the Shift key alone means nothing.
To receive notifications about key events, we need to provide a KeyListener
implementation. This interface has three methods “keyTyped”, “keyPressed” and “keyReleased”, each of them called when the respective event happens. (To avoid writing empty methods for the events we do not care about, create a subclass of KeyAdapter
, and override the methods for the events we do care about.)
The methods receive a KeyEvent
parameter containing information about which key was pressed. Its method “getKeyCode” returns one of the “KeyEvent.VK_…” constants; for example if Shift was the pressed key, value “KeyEvent.VK_SHIFT” would be returned.
If we process the key successfully, we should call the “consume” method afterwards to make this known to the Swing library. The code for creating a KeyListener
and registering it with the Canvas
could look like this:
canvas.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (KeyEvent.VK_SHIFT == e.getKeyCode()) {
e.consume();
... // update the game state
canvas.repaint();
}
}
});
There is one more technical detail: to receive key events, the canvas needs to have focus within the window. This is achieved by calling its “requestFocusInWindow” method after displaying the window, like this:
var canvas = ...
var frame = new JFrame("...");
frame.add(canvas);
frame.setVisible(true);
canvas.requestFocusInWindow();
Okay, now we are ready to put all these pieces together and create the first game…
An example game
Based on the previous post and this one, I wrote a simple puzzle game and published it at gitlab.com/kittenlord/puzzle. Check out the code and try the game!
At the beginning, the game shows you a picture. When you click “Game | Shuffle”, the picture is cut into small rectangles, which are randomly shuffled. The part that was in the bottom right corner is removed, so there is a gap, and you can move the remaining parts using the arrow keys on the keyboard. If you succeed to place all pieces in their original positions, the entire picture is shown again.
You can adjust the game difficulty by choosing the number of pieces in menu “Game | Set Difficulty”. You can also choose your own image, in menu “Image | Open File…”.
Let’s look at the code. The easiest part is the Direction
class, an enumeration containing four values: UP, RIGHT, DOWN, LEFT. It translates between these directions and the X and Y coordinates on the screen. (For example, moving right means increasing the X coordinate, moving down means increasing the Y coordinate. The coordinates in the Swing library start at the top left corner.)
Class Position
is an immutable pair of two integers: the X and Y coordinates. It can calculate that the position to the right of (2,5) is (3,5), and the position below (2,5) is (2,6). It can also enumerate all positions in given rectangle, and choose a random position.
Class Resources
provides the game resources: a default picture for the puzzle, an application icon, and a help text.
Class ImagePieces
handles cutting of the image into smaller rectangles. Please note that sometimes the size of the image cannot be divided exactly. Suppose the original image is 500×400 pixels large, and we want to cut in into 3×3 pieces. Dividing 500÷3 we get 166⅔, and dividing 400÷3 we get 133⅓. Instead of trying to work with noninteger image sizes, we simply round the numbers up, and create pieces of size 167×134. But of course a 3×3 grid of 167×134 images is 501×402 pixels large, which is more than the original image! In such case, we add a few extra black pixels on the right and bottom sides of the image.
We create an instance of ImagePieces
by providing an image and a resolution (how many small pieces we want to have along each dimension). Methods “getPieceWidth” and “getPieceHeight” returns the size of the individual pieces, and methods “getTotalWidth” and “getTotalHeight” return the size of the whole image, including the possible extra pixels. Method “cutPiece” cuts the pieces, and also adds a very simple 3D border (just four lines along the edges). Finally, method “draw” paints the specified piece at given coordinates on the game canvas.
When the player chooses another image or another resolution, the existing instance of ImagePieces
is simply thrown away and a new instance is created.
Class GameState
describes the state of the game. The game can be in one of three phases: the preview before the game (the image is cut into pieces, but not shuffled yet), the game itself (the pieces are shuffled and one of them is removed), and the victory screen (the original image is displayed). During the game itself, we need to remember the positions of all the pieces of the image.
Method “move” moves a piece in given direction, if possible. Method “shuffle” shuffles all pieces. Method “drawTo” paints the state of the game on the provided canvas.
As an example how saving and loading the game could be implemented, there are also methods “exportData” and “importData”, which convert the game state to a String and vice versa. This is a silly example, because in real life no one would bother to save and load this kind of game. Which is why I didn’t bother to actually save the data on disk. The export is merely displayed as a text, you can copy it and store into a text file if you wish, and then you can copy it back to restore the game state.
Finally, class Game
puts this all together. It creates the game window, the menu and the canvas. It manages the game state and the image pieces. It responds to key presses by converting them to directions and modifying the game state. And it handles the functionality of all menu items.
What next
Trying to modify this game doesn’t make much sense, I suppose. Just use its code as a reference.
Try to design your own game; something which could be played using the keyboard only, and which contains no animations, so everything in the game is like “press a key, the situation on the screen changes immediately”. Describe the game state and all the actions it allows in a class. Create the game window and respond to the keyboard in another class. Use more helper classes if necessary.
As you will see later, if you can do this, supporting mouse clicks and even animating objects in the game is not fundamentally different. Mouse clicks and timer ticks are just another form of an event.