Building Incremental Games as Web Applications
On this page
Have you ever wanted to make an idle clicker game like this?
Click at least three times.Good! Now try to get your number up to 100. Great job! Can you make it to 1,000?
I really like these kinds of games. There’s something about the predictability of numbers-go-up combined with exploration and unfolding that my brain is deeply attracted to. Lately I’ve been experimenting with different kinds of clicker game ideas, and when I do, I reach for starter code to get me up and running quickly that is very similar to what we are building in this tutorial.
My definition of an idle clicker game (or “incremental” game) is one that has at least three features:
- It should have at least one way to generate a resource
- It should have at least one way to spend that resource
- It should have at least one way to automate the generation of resources
In this guide, I’ll walk you through how to start building a basic web-based idle clicker game like the one above that leverages Vue, Vite, Pinia, and TypeScript. By using web technologies for our prototypes or production games, we gain all the affordances of a modern browser coupled with an unparalleled distribution strategy (e.g., share a link to the game instead of having someone download and install a game).
By the end of the tutorial, you’ll have a foundation from which to build your own web-based incremental game—and hopefully some inspiration to start making it your own!
We cover the following topics:
- Setting up your project
- Defining types to organize the game’s resources
- Defining a Pinia store to manage the game’s state
- Helper functions for computing resource costs and affordability
- Vue components to display state data
A follow-on tutorial will expand on this template, and add to our game:
- Researchable items
- Unlockable items
- Event-based narration updates
For now, the goal is to keep it simple.
We’ll be using the following computer instruction technologies:
Tool | To follow along, you should be… |
---|---|
Vue | Familiar (can read and understand what’s going on) |
Vite | Somewhat familiar (have used or could use) |
Pinia | Vaguely familiar (have heard of) |
TypeScript | Familiar (can read and understand what’s going on) |
Demo and code
⏭️ If you’d like to skip the tutorial and just get started with the template, it’s available here on GitHub.
💻 You can also play the demo of what we’re building before we begin.
Designing the game
We’re building a foundation from which to build our clicker game ideas, so it makes sense to think about what a “foundational” clicker game might be so we can figure out what needs to be built.
Every clicker game starts out with some thing to click. Clicking the thing generates some kind of resource, and a resource is something that can be exchanged for other things and/or resources. Additionally, there will be some kind of way to automation resource generation, sometimes called “auto-clickers”, for one or more resources.
The game we’re building in this tutorial will consist of these basic elements of an incremental game—which I have helpfully referenced here for you as The Three Laws of incremental games— implemented as energy, capacitors, and circuits.
Here’s the general outline of what the game will consist of:
-
You generate energy. The core resource is Energy; you’ll click a Create Energy button to create Energy.
-
Capacitors increase energy per click. You can spend energy to purchase (just another word for generate, if you think about it) capacitors. Each capacitor increases the amount of energy you generate per click.
-
Circuits auto-generate Energy. You can spend energy and capacitors on circuits. Each circuit automatically clicks the button that creates energy once per second. This allows players to generate energy even when they’re not actively clicking the button (e.g., while they have your game running in a browser tab somewhere in the background).
When finished, we’ll have a working prototype that is playable ad infinitum, with just enough scaffolding to start customizing and implementing functionality unique to the experience you want to create. It’s a foundation for a game moreso than a working game demo per se; it’s meant for you to finish this and then get to tinkering.
Setting up the project
Let’s start by setting up a project for our incremental game
with create-vue
, a tool for rapidly scaffolding a Vue
project:
If you’ve never used create-vue
before, you’ll be prompted
to confirm installation. Go ahead and proceed.
You’ll be prompted to enter a project name. I’ll be using
pinia-clicker
for the duration of this tutorial.
You’ll be asked whether you want to install TypeScript. Since this demo uses TypeScript, choose yes.
As for the rest of the questions, the only thing I included was Pinia. Here’s what my terminal looks like now that I’m done:
Follow the directions (cd
into your project dir and run
the commands above). When you’re finished, you will have
started the Vite development server and should be able to
open the link in your terminal to view your application.
With the project scaffolded, we’re ready to get started building the game.
Building the game components
Let’s start out defining some types that will give the game resources some structure. From there, we’ll create a new Pinia store to manage the game’s state and state mutations (e.g., when someone clicks, increment a resource, when someone purchases a resource, subtract the costs correctly, etc). Finally, we’ll create some Vue components to collect input and represent our game state to the player.
Defining types
Since type systems are syntactic methods of enforcing software abstractions —and writing software involves reasoning about different abstractions and their relationships—let’s start our game by defining some types.
In the src
folder of your project, create a new file called
types.ts
.
The first type we’ll create represents a resource cost:
What is [key: string]: number } & {
?
This is an index signature. Generally, index signatures are used when you don’t know the structure of a type ahead of time. We’re using it here as part of an intersection type so that we can have a type of optional (but known) properties.
Technically, Cost
is an intersection type comprised of of an index
signature ([key: string]: number
) and an object type with
optional properties ({ energy?: number; capacitors?: number; circuits?: number; }
).
const someCost: Cost = { energy: 10, circuits: 5, // note that the `capacitors` property is omitted }; console.log(someCost.energy); // Output: 10 console.log(someCost.capacitors); // Output: undefined console.log(someCost.circuits); // Output: 5
Something in this game can cost any combination of the following:
- zero or more energy
- zero or more capacitors
- zero or more circuits
Next, let’s create a type that represents the base costs of things that we can purchase:
Why is energy
optional?
Energy is the basic resource of the game, so I don’t want it to cost anything. Since a cost of some combination of energy, capacitors, and circuits, the resulting base cost for what is essentially a free resource would be:
energy: { energy: 0, capacitors: 0, circuits: 0 } as Cost,
Making it optional here with the ?
operator will allow us
to define base costs for capacitors and circuits and elide
a definition of energy costs.
Use this technique for other resources you add to the game that don’t cost anything to produce (e.g., maybe you have a research tree that just costs time instead of resources).
Why do we need to define base costs?
The base costs are used along with a few other variables to calculate the costs of each purchase. The formula—sometimes called a “growth formula” or “cost function”—goes like this:
Here, “cost multiplier” is synonymous with “rate of growth”.
For our game, we’re going to use a cost multiplier of 1.35
.
This is a completely arbitrary number; tweak it while you
play your prototype until you settle on a value that feels
nice, just make sure it’s always above 1.00
. The multiplier
is the rate of growth, so a larger multiplier is going to
produce a more drastic exponential increase than a smaller one.
To illustrate how this works, let’s say we set the base cost for circuits to 25 energy and 10 capacitors. Using our formula, the first circuit will cost 25 energy and 10 capacitors:
next energy cost = 25 * 1.35^0 = 25 next capacitor cost = 10 * 1.35^0 = 10
After we purchase our first capacitor, the cost of the next capacitor will be:
next energy cost = 25 * 1.35^1 = 33 next capacitor cost = 10 * 1.35^1 = 13
The last type we’ll define has to do with our game’s state:
These types give us an expandable structure from which to build an incremental game. In the next section, we’ll use them to implement the core features of our game.
Managing state
Let’s now implement our state management system with Pinia.
By the end of this part, you will have:
- a fully functional Pinia store to manage state in your game, including initial state, getters for retrieving state, and actions for mutating state
What is Pinia and why are we using it?
Pinia is a state management library for Vue. With Pinia, you create a place to store your data aptly called a “store”. Your stores have an initial state, getters for retrieving state data, and actions for mutating state data.
If you’d like to learn more about Pinia, I recommend the official documentation.
Inside the src
folder you’ll find that create-vue
has
setup a basic Hello World project for us. Since we opted in
to Pinia for state management during the setup process, there
is a src/stores/
folder with an example store counter
defined in counter.ts
.
Let’s rename src/stores/counter.ts
to src/stores/gamestate.ts
.
We’ll use this file to define a Pinia store that represents
and manages our game’s state.
Next, delete everything in that file, then import and use
defineStore
to start things off:
Here we begin defining gameState
, something we’ll import
into other parts of our game engine here to get and mutate
state.
A Pinia store has, in addition to an id
, three main sections:
- the intitial state of the store, called
state
- methods to retrieve values from the store, called
getters
- methods to mutate values from the store, called
actions
We’ll define these in order, starting with state
:
The state
property is where we describe our game’s state—
the variables we want to keep track of and be able to pass
into other parts of our game.
State property | What it’s for |
---|---|
id |
The unique identifier for the store |
energy |
The total amount of energy generated in the game |
capacitors |
The number of capacitors purchased; each increase the amount of energy generated per click |
circuits |
The number of auto-clickers purchased; each automatically click one time per second per unit purchased |
That takes care of our game’s initial state. Our next step is to define some methods to retrieve data from our game’s state manager. For the game we are building, we need to know the following:
- the total amount of each resource that we’ve either generated or purchased
- the next cost of something that is able to be purchased
- whether or not we can afford that cost
Before we define these getters, though, let’s build some helper functions based on the information we know we’ll need.
Generating the next resource cost
One thing we’ll need to know throughout this game is the cost of the next resource we want to purchase. I’m using the word “resource” loosely here. In your game, you may call it an “upgrade” or “unlockable” or something like that. Either way, the basic premise is the same: I should be able to see the amount of other resources I will need to purchase something.
At the top of src/stores/gamestate.ts
, just after the
import statement, let’s define a helper function that will
calculate the next resource cost of a given resource:
With this function, we’ll be able to pass along a reference
to our game’s current state and the string name of something
we can buy, then get a Cost
object in return containing the
cost of that purchasable item:
The state
parameter will come from Pinia; each getter will
be passed the current state so we can always get the most
current value of everything.
Checking if player can afford next resource
Knowing the cost of the next purchase with generateNextCost
,
we can now create a second helper function to return whether
we can actually afford it. This second helper function will
compare the next cost with the player’s current inventory, then
return true
or false
base on whether the player can
afford it:
With these two helper functions, we’re now ready to implement the getters for our Pinia store that holds are game’s state.
Getter functions
Our src/stores/gamestate.ts
file has two helper functions
followed by the beginning of our Pinia store. The store has
the state configured, and we’re now ready to get started on
the next section: the getter functions.
Getter functions are used to “get” variables from the state and bring them into our presentation logic—which, for this game, are Vue components.
Just like how the initial state is defined in a state
property, getters in a Pinia store are defined in a
getters
property. So let’s start by creating a getters
property that we can put our getters in, along with our first
getter:
Our first getter returns the total amount of energy in
our current state. Notice how we provide state
as a parameter.
In Pinia, getters rely on state, so any getter function should, at a
minimum, expect a state
.
Since capacitors represent how much energy is generated per click and circuits represent how many clicks are automatically made per second, let’s create a getter for each of those next:
Now let’s create some getters to provide resource costs.
We can start with a getter that leverages our generateNextCost
helper function to use the state
and a name of a resource
to calculate the next cost of something:
In the nextResourceCost
getter, we pass along the state
and also the name of a resource into generateNextCost
. While
this is a good general-use getter, we can create explicit
getters for the resources we already know we’ll need to know
the next cost of (i.e., capacitors and circuits) as well:
Just like when we built the helper functions, we not only need to know what the next resource costs are but also whether we can afford the next resource. Let’s build our final getter functions that leverage the other helper function:
All of these getters will be used when we implement the game’s Vue components (our presentation—or “view”—layer). But we’re not quite ready to move on to our presentation logic just yet!
While getters are used to get data from our game’s state, actions are used to mutate—or change—data in our game’s state.
In the next section, let’s define some actions that our game components will use to modify state values.
Action functions
The third and final section of our Pinia store’s definition is its actions. Actions are functions that modify state values.
There are three actions that can happen within our game’s state:
energy can be generated, a capacitor can be purchased,
and a circuit can be purchased. Each of these actions will
have their own separate function in our store’s actions
property.
Let’s go ahead and define them all now:
Notice how actions can call getters by using this
, like how
addCircuit()
first checks if the player can even afford a
circuit via this.canAffordCircuit
.
Each of these actions are generally related to a button that
the player can press, i.e., addCapacitor
will be called when
the player tries to buy a capacitor and addCircuit
will be
called when the player tries to buy a circuit. The dual-purpose
generateEnergy
function will either generate some amount
that’s passed as the amt
parameter or however much we
currently generate per click.
With the state, getters, and actions all defined, our Pinia store is complete—and with it, most of the logic for our game!
In our next and final step, we’ll bring all of this game logic onto the screen and build something we can start actually playing in the browser.
Game components
Our final task is to bring all of what we’ve created so far together into the presentation layer of our app. To do that, we’ll be building two Vue components: one for our main clicker button, and another for the current state of all our resources along with buttons to purchase capacitors and circuits.
By the end of this part, you will have:
- created a
Clicker
component that generates energy when clicked - created a
StoreDisplay
component that displays the current game state - integrated the Pinia store with the game components
Creating the Clicker
component
Let’s begin by creating the first button of the game.
First, go to the src/components
folder and delete the
placeholder Vue files (*.vue
). These are the files that
came with the project when we first set it up.
Then, create a new file called Clicker.vue
in the
src/components
directory, and add the following code:
This creates a new Clicker
component with a button that
generates energy when clicked. Notice how we can invoke the
Pinia store’s actions in the template after we import the
store in the setup script.
Wait—what are the elements of a Vue component file?
Using TypeScript with Vue’s Composition API, our files are going to be split into two parts (technical three, but more on that in a bit): the setup script and the template. They can be in any order. Review the Vue docs for more.
The setup script is defined in a script
tag, like this:
The template is defined in a template
tag, like this:
Put them together, and you get a full Vue component file:
There’s a third, optional part to a Vue component file,
which is a scoped style block. You can include component-specific
styling in a style
tag just like you would on a normal HTML
page. We’ll be doing this in the last section of this part.
This component gives us both our starting resource generation button and a template to use when you’re ready to add another button like it.
With our primary button finished, let’s now create a component to display all the game data variables we’re managing with Pinia.
Creating the StoreDisplay
component
The next component we’ll create displays all the state variables we want the player to know about and includes two buttons for making purchases (capacitors and circuits).
Create a new file called StoreDisplay.vue
in the
src/components
directory and add the following code:
Here we being the template by outputing some variables from
our game’s state, then we provide two buttons. Each button
has a set of span
elements for a label; the v-if
directive
will only display the element if the given condition true. These
spans assemble the full cost of something based on whether
or not it has any energy, capacitors, or circuits costs
associated with it.
Let’s deconstruct the second button that adds a circuit to see how it works.
First we create a button element, assign the addCircuit
action from the store to a click event, and disable the
button if ever canAffordCircuit
is false. The button’s
text label starts out with “Purchase Circuit (” and then we
move on to the dynamic span elements for cost:
If the next circuit cost’s energy property is not undefined (i.e., if there is an energy cost associated with purchasing the next circuit), then output the amount followed by the word “Energy.”
This pattern is repeated for the potential capacitor and circuit costs as well:
Then at the end, we close the button element.
Our component implementations are now complete, so now let’s register them with our app and do some end-of-tutorial tidying up to finish our game template. We’re almost done!
Integrating the components into the game
With our two components built, let’s now integrate them into our Vue app so that we can tinker and interact with what we’ve built so far.
Open the App.vue
file and replace the existing code with the
following:
The template here is where we lay out how the game
appears—which, for the purpose of being a template, is very
basic. This component is used in the project’s main.ts
file to
define our app and then mount it to an HTML element.
At this point, you’re ready to play your game!
Start the dev server, and open the link to localhost
emitted in your terminal window. For example, mine was:
Everything should be working as expected—but as soon as you
purchase a circuit (technically, one second after), you’ll
see we still have some more work left to do. We need a
way to account for changes to energy every second. We can do
that in the browser with setTimeout()
, but we’re actually going to
roll our own version more appropriate for a game’s needs.
Remember back in src/stores/gamestate.ts
when we created
those two helper functions, generateNextCost
and canAffordNext
?
I’m about to introduce a third helper function we’ll use
instead of setTimeout()
, which means there’s opportunity
now to clean up this game template we’re writing by creating
a dedicated space for helper functions. I wont put the two
we’ve already created in there so that you have something to
do in case you’re out of ideas but still want to feel productive.
Let’s create a new file called src/helpers.ts
, and in it,
define setTimeout2
:
If you’re curious about why I wrote a previous blog post about this function here.
To get our circuits generating energy every second, go to
App.vue
, import our new helper, and use Vue’s onMounted
method to handle passive energy production with our helper’s
help:
So every 1000ms, add the product of gameState.energyPerSecond
and gameState.circuits
to gameState.energy
.
Vue’s onMounted
lifecycle hook docs go into more detail,
but basically, when the component is ready to have things
done to it, it invokes this method and our timer starts.
Run the development server again, and buy a circuit. You should see energy accumulating passively.
Congratulations! You finished this tutorial, and in doing so, built your very own incremental game template. What feature will you add to it next? What game idea do you want to prototype and play around with? I’m excited to see what you build, so don’t hesitate to reach out and let me know when your game is ready to share.
Where to go from here
The basic features are here, but there is so much more than just generating resources and purchasing items that makes a great incremental game. What’s the next feature to implement? Where do you go from here? Well, I have a few ideas:
- Remember those two helper functions we created in the Pinia store file? Go put those into our dedicated helpers file, and update the imports appropriately so that the game still runs.
- In Action functions, the logic for calculating the costs of both circuits and capacitors is hard-coded to assume that circuits cost some combination of energy and capacitors, and that capacitors only cost zero or more energy. How could you modify so that you don’t have to hard-code these cost relationships?
- How would you implement a way to show (or hide) unlockable items based on state conditions (e.g., unlock something when you have X circuits)?
- How would you implement research items, with research time and in-progress updates?
- How would you implement a new resource “Electricity”
that costs X energy per second?
(how would you change
Cost
and how it’s used?)
Some of these things will be available in the next version of this tutorial—a free online book, just like my other ones.
All source code for this project is available here.
Questions? Comments? Feedback?
Please open an issue or reach out on BlueSky.
If you enjoyed this tutorial, feel free to show your support by buying me a coffee, starring the demo project on GitHub, or saying hello on Mastodon.
Have fun!
–Jesse