This game’s Java code uses records, so we will use Java 17. (Technically, the records were introduced in Java 14, but I prefer using the long-term support versions.)
The amazing thing about records is that when you use immutable classes, they can keep the code really short. Like, this short:
public record Position(int x, int y) { }
This is an entire class, actually used in this game. In just one short line of code!
Well, technically, you also need to declare the package containing the class. And maybe add a comment. And then the code formatter might insist that there should be an empty line between the package declaration and the class definition, and that the closing curly bracket deserves a separate line. But you get the point…
This definition creates the constructor and the getters (kind of), plus you get methods equals
, hashCode
, and toString
for free. Two positions are equal when their x’s are the same, and their y’s are the same. So I believe this is worth installing a new version of Java, which may also require a new version of your favorite IDE.
A maze with locked doors
Today we will make a maze, in form of a rectangular grid, where each square is either a wall or a free space. Using keyboard, the player can move up / down / left / right through the free spaces (i.e. not diagonally). One free space is a starting position; the player starts there. Another free space is an ending position; when the player gets there, the level is completed.
To make the game a bit more complicated, we will also introduce doors and keys. From the perspective of moving, a door acts like a wall, until you pick up a key, and then the door acts like a free space.
There will be different kinds of doors and corresponding keys. For example, picking up a red key opens a red door, but green and blue doors remain closed. Later, picking up a green key opens the green door, so now both the red and green doors act like free spaces, but the blue door still acts like a wall. Now we can put some keys behind other doors, and let the player open the doors in a sequence.
A picked up key disappears from the screen. (So when a red key is picked up, we need both to remove that key from the map, and to remember that “the red door is open”.)
What happens if there are multiple keys or multiple doors of the same color on the map? In this game, picking up any red key opens all existing red doors. So if there are multiple red keys, picking up the second one doesn’t change anything from the maze-solving perspective, because all red doors were already open. The only effect is that the second key now also disappears from the map. So you could have one key that simultaneously opens two doors; or you could have two alternative keys such that either of them opens the door.
(It was also possible to make an opposite design decision, to make one red key open one red door, and you need to pick up a second red key to open another red door. I decided against this, because it is more complicated. From the perspective of code, you need to remember which specific doors on the map are open, rather than just remembering generally which kinds of doors are open. You also need to display somewhere on the screen how many keys the player currently has, rather than opening the door immediately when the key is picked up. But most importantly, from the gameplay perspective, it would allow designing maps where the player can get stuck by wasting a key on a wrong door. (Which never happens when picking up any red key opens all red doors on the map. There are no possible mistakes to make, other than walking a few steps in a wrong direction, which can be fixed by walking back.) Thus we would also need to add an option to restart the level when the player gets stuck.)
Ultimately, both design decisions are valid, but we should consider their consequences first, before writing the code. A game where the player gets stuck in the middle, without the option to restart the level, would be very annoying.
Pictures are nice, even if they change nothing
So far we have described the mechanics of the game: walls, free spaces, doors, keys. At this moment, some authors would write the code, make one picture of a wall, one picture of a free space, maybe three different doors and three corresponding keys, design ten levels of increasing complexity, and call it a day.
There is a subtle lesson that is easy to miss. I realized it when I played “Knytt” (a nice platformer game created by Nifflas; sadly, the author’s web page is currently down). There is a difference between what things do, and what they look like. The difference can make the same game look 10× more interesting.
We are going to make a game where the player walks through “free spaces” between the “walls”. But what exactly are the “walls” and “free spaces”, visually?
Perhaps the “wall” is a literal wall, and the “free space” is a pavement. That would make it seem like walking in some kind of unfinished or abandoned building.
Or maybe the “free space” is grass, and the “walls” are trees. The map is a forest.
Or maybe the player is a ship, the “free space” is water, and the “walls” are rocks.
Or it could be a spaceship, and the “walls” are planets or meteoroids.
Mechanically, all of these are “walls and free spaces”, and yet, playing the levels with different pictures would feel differently.
If you make an unusual choice, it will make your game more unique.
If you make different choices in different levels, they will feel less repetitive. (Not necessarily a different design for each level, but maybe three forest levels, followed by three water levels, etc.) The progression can create a story — first you walk through a forest, then you find a ship, and then…
You can use different kinds of “walls” and “free spaces” on the same map. For example, you could walk around a village, stepping on grass and sidewalk, avoiding houses and trees and fences and wells and rocks etc. This would make the levels feel richer, and it only requires a little change in the code: supporting multiple pictures for each type of an object. The code is agnostic about specific pictures; they are listed in a configuration file.
(There is also an opposite opinion saying that using different pictures for functionally the same thing only distracts the player from what is important. This, too, is a valid design choice. You need to choose the kind of the game you are making. For a very difficult puzzle, it is probably better not to distract the player, and keep the level visually uncluttered. For an easy game, distraction could be the entire point.)
A 2D array of lists of game objects
The map of the game can be described as a rectangular grid, where each square in the grid can contain multiple objects. For example a red key laying on a grass would be described as a grass object (a free space) and a red key object (a key) at the same position. The order of the items at the same position is important: the grass is on the bottom, and the key is on the top. Otherwise we wouldn’t see the key.
Visually, the picture of the grass would be a solid rectangle, so that when we put grass objects on adjacent positions, they create a continuous lawn. The picture of the red key would be partially transparent, so that we see that it is a key laying on the grass (as opposed to a key laying e.g. on a pavement). Generally, it makes sense for the object on the bottom to be solid, and for remaining objects to be partially transparent.
It is technically possible to place any sequence of objects at any position, although some combinations would not make sense. (If you place a key onto a wall, it cannot be picked up, because the wall prevents the player from entering the position.)
There are several types of objects — free space, wall, door, key, player, exit — all implemented using the same Java class (GameObjectConfig) with different properties, specified in the configuration file:
Each game object is a free space, unless specified otherwise.
A game object with property “
isSolid = true
” is a wall. A position containing a wall cannot be entered by the player (regardless of what other game objects might be at the same position).A game object with property “
keyCode
” is a key. When the player enters a position with a key, the key is removed from the map, and its code is added to the collection of “open locks”. (The code is a text property, for example a red key would have property “keyCode = red
”.)A game object with property “
lockCode
” is a door. A door is considered a free space (and is not displayed on the screen) when its code is in the “open locks” collection, and it is considered a wall when its code is not in the collection.A game object with property “
isExit = true
” is an exit. When the player enters a position with an exit, the level is completed. (Note that you could have multiple exits, and entering any of them would end the level.)A game object with property “
isPlayer = true
” is a player. There can only be one player in each level, but you can use different player objects in different levels (that is, different pictures, for example a human in one level, and a ship in another level). The player moves when the user presses a key on a keyboard.
Each game object has a picture, and a “map code”. The map codes are used to define the level in a text file. For example, if we have a wall with map code “W”, and a grass with map code “g”, we could define a level like this:
W W W W W
g g g g g
W W g W W
If there are multiple game objects at the same position, concatenate their map codes. For example, if we also have a player with map code “&”, a red key with map code “r”, a red door with map code “R”, and an exit with map code “x”, we could define a level like this:
W W W W W
g& g g gR gx
W W gr W W
Note that the starts of the grid columns are vertically aligned, and there is an empty column between them in the text file. This is required by the level parser.
A map code can be more than one character. For example, a red key and a red door could have map codes “kr” and “dr”. Or you could have several different trees with map codes “t1”, “t2”, “t3”. The only condition is that one game object’s map code cannot be a prefix of another game object’s map code (i.e. don’t have “x” and “x1”).
Configuration loader
In previous post I have suggested that instead of XML, JSON, YAML, or any other complicated format, we could keep the configuration in a simple Properties
file. That works nice when the properties are few. But with many properties, we have many places in the code that try to read them. Even worse, an attempt to read a property may fail (if the key is missing, or the value has a wrong format, for example if we expect a number), and it is too bad to find this out in the middle of a game.
It would be better to read the entire configuration file into a Java object before the game starts (and show an error message and exit the program in case of any error).
In case of XML and JSON, there are libraries for doing exactly this, without having to write repetitive code for “read property xyz into a variable called xyz”. The name of the property is by default the same as the value of the variable, so you just write “read the contents of this file into an instance of this class” and let the library do the rest.
I was curious how much work it would be to write the same basic functionality for the Properties
file, and here is my solution. (Perhaps I have overcomplicated things, and you should go with XML or JSON instead.) It works like this:
Class ConfigurationData is a simple wrapper for Properties
. In addition to reading a String value, it can also read an int value, or a boolean value. The boolean values can be written as “true
” / “false
”, or “yes
” / “no
”, or “on
” / “off
”, or “1
” / “0
”; any other value is considered an error. In addition to that, a missing key is considered to be false
. (Thus, instead of writing “isSolid = false
” for each object that is not a wall, you can simply omit the property.)
Class ConfigLoader does the actual magic. You tell it the class that represents the configuration file (the class must be a record). It creates an instance of that class, reading the value of each member variable from the corresponding property. For example, if your record is like “String name, int size, boolean isVisible
”, it will get the value for name
from the property “name”, the value for size
from the property “size” (and fail if that value is not numeric), and the value for isVisible
from the property “isVisible” (and fail if that value is not boolean).
Furthermore, if the member variable is of the type “list of records”, the loader will find properties starting with the variable name, and recursively create the records in the list. For example, if the parent record is like “List<LevelConfig> levels
”, and the child record (LevelConfig) is like “String fileName, int number
”, it will find all properties with names like “levels.something.etc”, creates instances of the child record, and get the value for fileName
from the property “levels.something.fileName”, and the value for number
from the property “levels.something.number”. (The value of the placeholder “something” is irrelevant here; it is only used to group the values for the same child record together.)
If this explanation confuses you, just look at the source code: the configuration file “config.properties” and the classes Config, GameObjectConfig, LevelConfig; it should be obvious how the values are supposed to be loaded. Then look at ConfigLoader, which tries to do exactly that. See also ConfigLoaderTest.
(Note that ConfigLoader
only supports member variables of types that I actually needed for this game: String
, int
, boolean
, and List
of records. If you want it to support another type, you need to update the code. I was not trying to make a fully general library, just a proof of concept. Actual libraries of this kind have a lot of extra functionality, such as overriding the default functionality using annotations, or adding custom types. This is probably already too complicated for a game-making tutorial.)
The game code
The source code is available at gitlab.com/kittenlord/demo004.
Class Position, a record of integers (x, y) represents a position on the map.
Class Direction represents one of the basic directions on the screen (up, down, left, right), and helps to find the next position in given direction.
Class Resources contains static methods for reading game resources: the configuration file, images, and text files (containing level definitions). It also provides a method to convert white pixels to transparent, making it easier to create game images using simple editors that do not support PNG transparency.
Classes Config, GameObjectConfig, LevelConfig contain the game configuration.
Class LevelLoader parses the level definition — converts a text file into a map of lists of game objects.
Class GameState contains the game state and the game logic. The game state is a map of lists of game objects, plus information about which kinds of doors are already open. The game logic checks whether the player can move in a certain direction, moves the player, checks whether the level is completed, and loads the next level.
Class GameDialog creates the game window, loads all images, displays the current game state, and react on keys pressed by the user.
Ideas for your experiments
First, play the original game. The levels are few and simple. Check the text files where the levels are defined.
Try changing the existing levels, and test them by playing.
Replace the existing levels with your own. (Hint: While you are working on a new level, call it “level00”, so that it starts immediately when you run the program.)
Replace the existing objects with your own. All object images should have the same size, called “objectWidth” and “objectHeight” in the configuration file. The size of (the interior of) the game window is called “windowWidth” and “windowHeight”; this should also be the size of the title image, called “titleImage”. Feel free to change the dimensions, just remember that the sizes determine how big you can make your maze. For example, if whe window size is 800×600, and the image size is 100×100, you can only have 6 rows and 8 columns maximum (though it is okay to have less, that’s what the “backgroundColor” in level configuration is for).
The game currently has a title screen, but no ending screen. Try implementing a proper ending screen:
Update the
GameState
. Currently, a variable “currentLevel” contains the index of current level, with null value meaning the title screen. Instead of this hack, implement the screen type properly by adding a separate variable “screenType”, an enum with valuesTITLE_SCREEN
,LEVEL
,ENDING_SCREEN
. (Maybe alsoHELP_SCREEN
andCREDITS_SCREEN
?)Update the code in
GameState
andGameDialog
to handle this new variable properly. Completing the last level sets the “screenType” toENDING_SCREEN
. Pressing space during the end screen switches to the title screen. (Pressing F1 during the game or the title screen switches to help screen. Pressing Esc during the help screen switches back to the game or the title screen, depending on the “currentLevel” variable.)Create a new picture for the ending screen, a new variable in
Config
, and add the information to the configuration file.
Advanced: Make a level editor, as a separate dialog. (I tried, but then gave up.)
Is this a browser plug-in?