Let’s review the previously covered concepts of game programming that are also relevant for today’s game:
game state
events
configuration file
saving and loading data
Today we won’t need the key and mouse events, or any work with images besides loading them, because this game will use a Swing GUI, like a serious Java application. (The game also won’t use sounds.) The new part is that the game will use time.
The previous games have waited until the player clicked a mouse button, or pressed a key. Without the player action, nothing happened; the game kept waiting.
Today we will make a game that is the opposite of that, where passing of time is the most important thing, and the player mostly just watches the numbers go brrrrr. This genre is called “clicker games” (because you still need to click on something once in a while) or “incremental games”.
Time is just another event
Generally speaking, passing of time is just another kind of event, and we handle it the same way. A mouse button is clicked: update the game state, refresh the screen. A key is pressed: update the game state, refresh the screen. A time interval has passed: update the game state, refresh the screen.
There are two important differences, though.
First, time is continuous. To make events out of it, we need to slice it to time intervals, and it is not obvious what is the right way to do that.
With keyboard, we don’t have this kind of problem; key presses are discrete by nature. So are mouse clicks. It is different with mouse movement, which is also continuous, but the operating system has already made the choice for us, and provides us with discrete “mouse moved” events. With time, the choice is up to us.
The standard time units, seconds and milliseconds, are not good choices here; and they are arbitrary anyway. A second is too long; an animation updated in one-second intervals would be extremely jerky. A millisecond is too short; the human eye cannot process changes so quickly. Also, updating the game state and redrawing the screen in one-millisecond intervals would assume that we can actually accomplish all of that during one millisecond, which is probably a too optimistic assumption. Anyway, the computer screen itself is only updated in certain intervals; it does not make sense to redraw it more frequently, because some changes will simply never be displayed.
Actually, the refresh interval of the screen might be a natural choice; the problem is that each screen has a different interval. Also, we would have to synchronize our changes with the screen updates, and I really don’t want to go there right now.
In practice, it seems like most people don’t obsess over this topic as much as I do, and simply make an arbitrary choice, such as 1/10 of a second, and… it works okay. And if it does not, simply adjust the number until it does.
What I would recommend here is to make the time interval a constant in code (or maybe a parameter that is loaded from a configuration file?), and include it in all calculations in a way that allows you to change it later without breaking the game. For example, don’t say that a character moves “2 pixels per time interval”, because if you later change the time interval, it will make the character move slower or faster. Instead, say that your character moves “20 pixels per second”, and then if you specify that the time interval is 1/10 of a second, you can calculate that the character moves 2 pixels per time interval. And if you later change the time interval to 1/5 of a second, the same calculation will make the character move 4 pixels per time interval, which will result in the same subjectively perceived speed.
(I have heard that the pros distinguish between time intervals for calculating the game logic, and time intervals for redrawing the screen. For example, you could decide that the game is updated 100 times per second, but only each fifth update is displayed on the screen. On faster computers, you could display each second update, or maybe all of them. So the movement would seem more fluent, but the underlying calculations would be the same, which would prevent the game from behaving slightly differently on different computers.)
By the way, if you already update the screen 10 times or more per second, you don’t really need to also update it after key and mouse events; only update the game state. The screen will be updated a moment later anyway.
Second, playing with time introduces the topic of threads. A computer can do multiple things at the same time. Java supports this, and it is not too difficult… if you know exactly what you are doing. But my advice for beginners is: don’t.
Try to keep things in the same thread as much as possible. You don’t want to update the game state at the same time you are painting it on the screen, which could result in inconsistent things being painted. Even worse, if the update gets too complicated, or just the computer suddenly slows down a lot (e.g. because Windows just started downloading operating system updates from internet), you don’t want to start the next update before the previous one is over; that could completely mess up the data.
There are two timers in standard Java: “java.util.Timer” and “javax.swing.Timer”. The former is more precise, and creates new threads. The latter is less precise, and uses the same AWT/Swing thread that all other events use (e.g. key pressed, mouse button pressed, mouse moved, component painted). Use “javax.swing.Timer” to keep your application single-threaded, and you will avoid various mysterious bugs that can happen as a consequence of an incorrectly written multi-threaded code.
The game model
In this game, the player is an alchemist trying to create new things from the elements, and use them to obtain even more elements. The basic elements are: air, water, earth, and fire. Additional resources used in alchemy are: salt, iron, silver, and gold.
The resources are listed in class Resource. There is also a limit; a player can never own more than 9999 9999 units of each resource; additional resources will be lost.
(As an arbitrary artistic choice, I decided to group digits by four, as opposed to the usual Western system of grouping digits by three. We used to have a word “myriad” meaning 10 000, but then it somehow felt out of use, and got replaced by the powers of thousand, even those inconsistently. Some languages still use myriad, and so will the player of this game.)
The resources can be spent to buy what is traditionally called “factories”, i.e. things that produce more resources. In this game, they are represented by animal names: the air factories are: Butterfly, Albatros, and Pterodactyl; the water factories are: Dragonfly, Octopus, and Kraken; the earth factories are: Beetle, Turtle, and Dinosaur; and the fire factories are: Firefly, Phoenix, and Dragon. (There are no separate factories for salt, iron, silver, and gold, but that’s just my choice. There is nothing in code that would require it to be that way.) The factories are listed in class Factory.
In addition to factories, the player can also buy various upgrades, listed in class Upgrade. They usually double the production of something, or introduce production of an extra resource. There is a special upgrade called “Reincarnation” which upon purchase completes the game… or rather restarts it, giving you a 10% production bonus in your next life.
Classes Resource, Factory, and Upgrade are mere lists of items. The actual economical (alchemical) relations are in separate classes
All costs in this game are expressed in the resources; for example, something could cost “10 gold” or maybe “5 gold and 10 silver”. Class ResourceCollection represents a collection of various amounts of different resources. It is a wrapper for a Map from Resource to BigDecimal, with extra methods that make it simple to use.
When you create a new ResourceCollection, the amounts of all resources are initialized to zero (to avoid worrying about null values), but method “toString” only returns the resources where the amount is non-zero. Method “get” returns the amount of the specified resource. Static methods called “of” allow you to quickly create “a collection of X units of this and Y units of that” etc.
The amount of resources is expressed as a BigDecimal. In hindsight, this feels like overengineering; I probably could have gone with Double or even Float instead. To keep things simple, all basic costs and productions are actually integers. But decimals come into play with the time intervals (the production is defined in resources per second, but it is updated after fractions of a second) and the 10% production bonus after completing the game, so the intermediate calculations need to be done with decimal numbers. I was worried about rounding errors, but that actually doesn’t make sense; the resources keep increasing all the time, so the only impact of a rounding error is that it will take a fraction of a second longer until the player can buy something.
The class ResourceCollection is immutable; all mathematical collections return a new instance of the class. (This is modeled after BigDecimal.) Methods “plus”, “minus”, and “multipliedBy” do the simple math; “setScale” sets the number of decimal places; and “setMaximum” limits the amount of each resource in the collection.
Method “containsAll” checks whether another collection is a subset of this collection (for example, “5 gold and 10 silver” would contain as subsets “1 gold” or “2 gold and 7 silver”, but wouldn’t contain as subsets “1 iron” or “11 silver”). This is important to check before subtraction, because the “minus” method throws an exception if the result would contain a negative amount of some resource.
Finally, the economy: Class UpgradeCosts contains the costs of the individual upgrades. It is completely straightforward; all costs are fixed.
(You could make this more complicated, and make the costs of upgrades depend on the state of the game. For example, an upgrade could get cheaper the more factories you own. Or you could have two upgrades such that buying either of them makes the remaining one more expensive.)
Class FactoryCosts is more complicated, because the costs of factories increase as you buy them. For example, the first Butterfly costs 10 air, the second one costs 11 air, the next one costs 12 or 13 air, etc. Otherwise, after buying the first few factories, buying the remaining ones would be too easy; your wealth would grow exponentially.
The maximum number of each factory you can own is 9999. There are buttons to buy 1, 10, 100, or 1000 of them at the same time. Which made me worried that maybe calculating the cumulative cost of 1000 factories would take too much time, especially if we need to update the information for dozens of buttons in a fraction of a second. (Perhaps I shouldn’t have worried about that. The computers are fast these days. Also, we don’t have to update the costs of all buttons all the time, only the ones that have changed now as a result of some purchase. But the code is simpler if we keep updating everything.) A mathematically elegant solution I figured out is that instead of having a formula for “how much does the n-th factory cost” we could instead have a formula for “how much do the first n factories cost together”. And then, instead of thousand additions, we only need to do one subtraction. For example, you already have 123 factories, and you want to buy 1000 more. Instead of calculating cost(124) + cost(125) + … + cost (1123), we calculate cumulativeCost(123 + 1000) - cumulativeCost(123). Now all we need is a mathematical formula that has the following desired properties:
f(1) - f(0) = 1, so that we simply multiply that by the cost of the first factory,
f(x) keeps growing faster than x, at least for x between 0 and 9999,
but shouldn’t grow too fast, or we won’t be able to afford the 9999th factory.
The formula used in the game is “f(x) = 0.95 x + 0.04999 x² + 0.00001 x³”. Sorry if this is too complicated; the idea is that the sum of the coefficients is equal to 1, the first one should be the largest, and the last one very small. That way, the costs keep growing, slowly at the beginning and faster later.
You know what? Just throw it all away and write something else. I have spent a lot of time rewriting this, and now I am happy to have something that works.
(In a typical incremental game, the costs would grow exponentially. But that is because a typical incremental game expects you to spend a lot of time playing it. Most of that time is spent waiting anyway, and you just switch to a different application, or even turn off the game completely… and the next time you turn it on, the game checks how much time has passed, and gives you the corresponding production. Instead, I wanted a game that can be completed in about an hour or two. Which is why the cost growth formula is polynomial instead of exponential: you are expected to actually buy a few thousands of factories in a few hours, and then win. For those who want to keep playing endlessly, there is the restart-with-a-bonus option.)
There is a unit test FactoryCostsTest that makes sure that:
the first factory of each kind costs as much as expected,
the costs of factories do not decrease as you keep buying them,
the cost of the last, most expensive factory does not exceed the maximum possible amount of resources.
If you change the factory cost formula, make sure to run this test afterwards.
Class FactoryProduction calculates the production of the factories you own. The calculation is linear (two factories produce twice as much as one factory), but the production of individual factories may depend on the upgrades you own.
There are upgrades that double the production of a specific kind of factory. For example, at first a Butterfly produces 1 unit of air per second, but a “Double Butterfly” upgrade changes it to 2 units of air per second.
There are upgrades that enable additional resource production of a specific kind of factory. For example, at first an Albatros produces (per second) 5 units of air (10 with “Double Albatros”) and 1 unit of water, but after buying an “Albatros also produces salt” upgrade, it also produces 1 unit of salt.
There are upgrades that double the production of a specific resource for all factories that produce it. For example “Double all salt production” does what it says.
There are upgrades that double all production of all resources: the three pyramids.
The upgrades are multiplicative. For example, the Albatros starts producing 5 air and 1 water, but after buying “Double Albatros”, “Albatros also produces salt”, “Double all salt production”, and the three pyramids, it will produce 5×2×2×2×2=80 units of air, 1×2×2×2=8 units of water, and 1×2×2×2×2=16 units of salt, per second.
Now multiply that by the number of Albatroses you own, which is probably over a thousand at the point of the game when you own the pyramids, add the production of other types of factories, and it will only take a few minutes to reach the maximum of the resources and win the game.
Class State encapsulates all of this. It remembers how much resources, how many factories, and which upgrades you have. (Also how many times you have restarted the game and how big production bonus you get for that.) Its most important method is “update”, which increases the resources by the amount produced by the existing factories during the given time interval.
Methods “saveTo” and “loadFrom” store all the data in a Property object.
Notice that the functionality of the “Reincarnation” upgrade is not described here. From the perspective of this package, it seems like a thing that can be bought and… does nothing. (There is a method “restart” in class “State”, but there is no connection between the method and the upgrade.) This will be handled in the user interface.
The game dialog
Unlike the previous games, where we usually created a JFrame object containing a Canvas object, and then handled most of the user interface in the canvas, this game will use more controls provided by the Swing library. Let’s review them, briefly.
JLabel
is a text displayed in a dialog.
new JLabel("Hello World!");
JButton
is a button that contains a text (possibly also a picture, but we do not use that now), can be enabled or disabled, and calls a provided method when clicked. When we position the mouse cursor over the button, a tool tip text can be displayed.
We can assign functionality to a button like this:
var button = new Button("Click me!");
button.setToolTipText("If you click this button, something will happen.");
button.addActionListener(e -> doSomething(...));
(The parameter “e” in this call is an event object, which contains information about how specifically the button was clicked, but we do not need it now. It could be used to determine which button was clicked, but that is easier to achieve by calling different methods for different buttons, or calling the same method with different arguments.)
JTextField
is a text input field that typically allows the user to enter a text. In this game, we will set it to read only, and only use it to display numbers. (We could have used a JLabel instead, but I like the thin frame around the numbers.) We can set the font, and also align the contents to the right, which is better for numbers.
JTabbedPane
is an area with multiple tabs on top. Each tabs shows different content.
var tabs = new JTabbedPane();
tabs.add("First", contentsOfTheFirstTab);
tabs.add("Second", contentsOfTheSecondTab);
JPanel
is a group of components arranged together. (Panels can also contain other panels.) There are different strategies for arranging components in a dialog, called layouts. They allows us not only to provide the initial position of the components, but also their behavior when the dialog gets resized (which controls will stay the same, which ones will move, and which ones will expand/contract). There are a few different layouts provided by the Swing library, and each panel can have a different one. But to keep things simple, we will only use one kind of layout for everything.
BoxLayout
arranges components in a line, vertically or horizontally. We can specify how the components are aligned in the perpendicular direction. (For example, if we place components with different heights on a horizontal line, should their tops align? Or their bottoms? Or should they be centered vertically?) We can put fixed space between the components (e.g. “this button, then 10 pixels space, then this button”). We can also put a flexible space between components, so when the entire panel is resized, this space will expand or contract.
To make this even simpler (it can shorten the code dramatically, if there are many components and panels in the dialog), we have a BoxBuilder class. Static methods “horizontal” and “vertical” get ready to prepare a panel with given orientation, with given spacing between adjacent components. Method “add(JComponent)” adds components, and the fixed space between them; method “addFlexibleSpace” adds the flexible space. All these methods return “this” as a result, which allows us to conveniently chain them. Finally, method “build” returns the panel. Like this:
BoxBuilder.horizontal(10).add(button1).add(button2).add(button3).build()
Using this class, we get the technical details of “how to put components next to each other in Java” out of the way, and we can focus on designing the game dialog. The dialog contains a few tabs. Each tab contains a few parts arranged vertically. Some parts display a resource (a picture, a label, a text field displaying the amount owned). Some parts display a factory (a label, a text field displaying the amount owned, buttons to buy more). Some part display a list of upgrades (buttons).
Class GameDialog creates all the controls, builds the game dialog, starts the timer, and updates the game state and the controls on timer events.
The timer is simple:
var timer = new Timer(intervalInMilliseconds, e -> doSomething());
timer.start();
...
timer.stop();
Updating the controls means calculating how much would it cost to buy the factories or upgrades, and enabling or disabling the buttons accordingly.
The method for buying an upgrade has an extra logic related to buying the “Reincarnation” upgrade, which restarts the game.
When user closes the game dialog, the game state is saved automatically. When user starts the game, the game state is loaded automatically.
Other classes
Class EnumMapUtil contains a static method to fill all the values in a map with zeroes. (We can use this to avoid a possible null pointer exception.)
Class GameSaveFile handles the details of saving and loading the game. Specifically, it determines the directory and the file where the game state is saved.
Class GameResources loads the configuration file and the images used in the game. It provides methods to get configuration values as a String, int, or Color.
Finally, class Main tries to load the game resources, and then either displays an error message, or starts the game dialog in the AWT/Swing thread.
Playing the game
CONTAINS SPOILERS — skip this section if you want to explore the game for yourself!
The gameplay is a loop: spend resources, to buy factories/upgrades, to gain resources.
But if you have no resources, how can you enter the loop?
At the beginning, there is a button called “Breathe” that allows you - the wise and mighty alchemist - to breathe some air (one of the four elements) into the system.
The button gets disabled after you buy a Butterfly, the first air factory. Don’t worry; now the Butterfly produces air. And the more Butterflies to have, the more air they produce together, so after a while your contribution would be a rounding error anyway.
(I didn’t want to make this the kind of game where you benefit from clicking as fast as you can. However, if that is the kind of game you want to play, maybe consider… not buying a Butterfly?)
Okay, so now you have air that produces more air. What about the other elements?
Check the other factories. In the Air tab, you have Butterfly, Albatros, and Pterodactyl. What do they cost, and what do they produce? You can’t buy a Pterodactyl, because it also costs gold, and you have none. But you could buy an Albatros. And the Albatros also produces a little water… and soon you can start also buying things in the Water tab.
And so on. Go play the game. If you ever think you got stuck, check all the tabs, and make sure that you have at least one factory of each kind you can currently buy. Then just keep buying all the upgrades, and more factories, in any order. Also, it takes some time (about an hour or two) to complete the game. After you have unlocked all the elements, just keep the game running, and return to it a few minutes later.
Too bad that this will only be interesting for the first time you play the game. Later you will remember the initial sequence of unlocking the elements, and only go through the motions.
(To make sure I didn’t accidentally change the game to make it unplayable, there is a StateTest containing a method “testEntireGame” that actually completes the entire game! It does so very inefficiently, by clicking little and waiting a lot, so it completes the game in 11 hours of simulated time. The actual time of running the test is short.)
Modifying the game
If you want to create your own incremental game, you could use this code and add more resources, more factories, more upgrades. Here are a few new rules that would only require very small changes to the existing code:
different resources or different factories could have different limits (you could have 1 000 000 air, but only 1 000 gold; or 100 butterflies, but only 3 dragons),
there could be upgrades that increase those limits,
some upgrades could be switches: buying upgrade A would make you lose upgrade B and vice versa,
you could gradually lose some factories (every minute one of your butterflies dies),
some upgrades could be temporary (“10× dragon gold for 5 minutes”),
the costs of factories and upgrades could be not just resources, but also factories (buying 1 wolf costs you 10 bunnies; sacrificing 1000 dinosaurs unlocks the buttons to buy tyrannosaurs),
some game options (buttons, tabs) could be hidden until you unlock them,
there could be different kinds of restart bonuses (you could get a 20% bonus on a specific resource, depending which upgrade you used to restart the game).
Some of these changes could significantly change the gameplay. For example, the version where you keep losing factories over time, and the upgrades are temporary, would require more active playing.
You will need to test the new game to make sure that the economy is balanced.
I mostly got lucky; I am surprised that the game works much better than I originally expected. In my first attempt, I only used air + water + earth + fire for the final upgrades, but then I realized that after unlocking all the factories, the salt + iron + silver + gold were practically useless. So I updated the pyramids and reincarnation to also cost some salt + iron + silver + gold, but then it took too much time to collect the salt required to complete the game, because too few things produced it. So I added salt production to a few more factories, and then… it seemed okay, so I stopped there.
(When testing the game, you can edit the save file to give yourself a huge bonus, so the test game will be much faster.)
Another thing you could do is improve the user interface, because currently it is quite ugly. For example, a disabled upgrade button could either mean “you don’t have enough resources to buy this upgrade” or “you already own this upgrade”. Currently, the tool tip text tells your which case it is, but it could be made more obvious, for example by adding an icon to the button, or by changing the button color.
A more heroic change - one that I attempted initially, and then I gave up and rewrote everything from scratch - would be to change the game so much that everything is in the configuration file, and nothing is hardcoded in Java. There would be no enumerations of resources, factories, and buttons; all would be loaded from the configuration file. Including the costs, productions, and the effects of individual upgrades. Something like this:
factory.albatros.name = Albatros
factory.albatros.cost.air = 100
factory.albatros.production.air = 5
factory.albatros.production.water = 1
upgrade.albatros-double.name = Double Albatros air production
upgrade.albatros-double.type = production-multiply
upgrade.albatros-double.amount = 2
upgrade.albatros-double.factory = albatros
upgrade.albatros-double.resource = air
upgrade.albatros-salt.name = Albatros also produces salt
upgrade.albatros-salt.type = production-add
upgrade.albatros-salt.amount = 1
upgrade.albatros-salt.factory = albatros
upgrade.albatros-salt.resource = salt
Each upgrade would have a type, such as “production-multiply” or “production-add”, which would apply to a selected factory (or all factories, if not specified) and a selected resource (or all resources produced by give factory, if not specified), and either multiply or increase the existing value by a specified number. That (and “restart”) is all you would need to describe the current game. But you could also add new types.
It would also be important to specify in which order the effects should be applied, because multiplying by 2 and then adding 1 is different from adding 1 and then multiplying by 2. (You could have a general rule that all additions are applied first, and all multiplications later.)
could i ever be on your website im https://www.youtube.com/@kittenlord101. I just want to let people know that this isnt my website because I believe it could be confusing
hey kittenlord