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–a primary resource generated per click (energy), a way to increase how much resource is generated per click (capacitors), and a way to auto-generate the primary resource (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:
npm create vue@3
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:
Vue.js - The Progressive JavaScript Framework
✔ Project name: … pinia-clicker
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … No / Yes
Scaffolding project in /Users/jesselawson/dev/pinia-clicker...
Done. Now run:
cd pinia-clicker
npm install
npm run dev
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:
export type Cost = {
[key: string]: number;
} & {
energy?: number;
capacitors?: number;
circuits?: number;
};
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:
9 export type BaseCosts = {
10 [key: string]: Cost;
11 } & {
12 energy?: Cost;
13 capacitors: Cost;
14 circuits: Cost;
15 };
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:
next cost = [base cost] * [cost multiplier]^[# owned]
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:
17 export type GameState = {
18 [key: string]: number;
19 } & {
20 energy: number;
21 capacitors: number;
22 circuits: number;
23 costMultiplier: number;
24 baseCosts: {
25 capacitors: Cost;
26 circuits: Cost;
27 };
28 };
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:
import { defineStore } from 'pinia'
export const gameState = defineStore({
id: 'gamestate',
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
:
5 // Describe the initial state of our store:
6 state: () => ({
7 energy: 0, // Start at 0 energy
8 capacitors: 1, // Start with 1 capacitor
9 circuits: 0, // Start with 0 circuits
10 costMultiplier: 1.35, // Set rate of growth to 1.35
11 baseCosts: {
12 capacitors: {
13 energy: 3 // Capacitors base cost is
14 } as Cost, // 3 energy
15 circuits: {
16 energy: 5, // Circuits base cost is
17 capacitors: 5 // 5 energy and 5 capacitors
18 } as Cost
19 } as BaseCosts
20 } as GameState),
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:
4 const generateNextCost = (state: any, resource: string): Cost => {
5 // Get the base cost for the resource:
6 const baseCosts = state.baseCosts[resource];
7
8 // Get how many of these resources we already own:
9 const currentResourceCount = state[resource];
10
11 // Iterate over baseCosts to dynamically calculate
12 // the next cost for each cost type (e.g., energy,
13 // capacitors, circuits). The reduce function helps us
14 // accumulate the next cost values into a new `Cost`
15 // object that we can then return:
16 return Object.keys(baseCosts as BaseCosts).reduce(
17 (nextCost: Cost, costKey: string) => {
18 // Get the base cost--or, if baseCosts[costKey] is falsy,
19 // default the base cost to zero:
20 const baseCost = baseCosts[costKey] || 0;
21
22 // Calculate the next cost by multiplying the
23 // base cost by the cost multiplier raised to the
24 // power of the current resource count, and wrap it
25 // in Math.floor() to ensure the result is an integer:
26 nextCost[costKey as keyof Cost] = Math.floor(
27 baseCost * Math.pow(state.costMultiplier, currentResourceCount),
28 );
29 return nextCost;
30 },
31 {},
32 );
33 };
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:
1 // For example:
2 const nextCircuitCost = (state) => generateNextCost(state, "circuits");
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:
18 const canAffordNext =
19
20 // Check if the player can afford the next resource
21 // based on the current state:
22 (state: GameState, resource: string): boolean => {
23 // Generate the cost of the next resource based on the current state:
24 const nextCost = generateNextCost(state, resource);
25
26 // Check if player has enough of each cost key:
27 return Object.keys(nextCost).every(
28 // For every cost key, see if the matching state key
29 // is greater than or equal (if yes, then player
30 // can afford):
31 (costKey) => {
32 // Get the current count of the resource,
33 // and default to zero if it doesn't exist:
34 const currentResourceCount = state[costKey] || 0;
35
36 // Get the cost of the resource,
37 // and default to zero if it doesn't exist:
38 const cost = nextCost[costKey as keyof Cost] || 0;
39
40 // Return whether the current count is
41 // greater than or equal to the cost:
42 return currentResourceCount >= cost;
43 },
44 );
45 };
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:
86 } as GameState),
87
88 // Define our getters:
89 getters: {
90 getEnergy: (state) => state.energy,
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:
86 // Define our getters:
87 getters: {
88 getEnergy: (state) => state.energy,
89 energyPerClick: (state) => state.capacitors,
90 energyPerSecond: (state) => state.circuits,
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:
86 // Define our getters:
87 getters: {
88 getEnergy: (state) => state.energy,
89 energyPerClick: (state) => state.capacitors,
90 energyPerSecond: (state) => state.circuits,
91 nextResourceCost: (state) =>
92 (resource:string) => generateNextCost(state, resource),
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:
86 // Define our getters:
87 getters: {
88 getEnergy: (state) => state.energy,
89 energyPerClick: (state) => state.capacitors,
90 energyPerSecond: (state) => state.circuits,
91 nextResourceCost: (state) =>
92 (resource:string) => generateNextCost(state, resource),
93 nextCapacitorCost: (state) => generateNextCost(state, "capacitors"),
94 nextCircuitCost: (state) => generateNextCost(state, "circuits"),
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:
86 // Define our getters:
87 getters: {
88 getEnergy: (state) => state.energy,
89 energyPerClick: (state) => state.capacitors,
90 energyPerSecond: (state) => state.circuits,
91 nextResourceCost: (state) =>
92 (resource:string) => generateNextCost(state, resource),
93 nextCapacitorCost: (state) => generateNextCost(state, "capacitors"),
94 nextCircuitCost: (state) => generateNextCost(state, "circuits"),
95 canAffordCircuit: (state) => canAffordNext(state, "circuits"),
96 canAffordCapacitor: (state) => canAffordNext(state, "capacitors"),
97 },
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:
102 // Define actions that mutate state values:
103 actions: {
104 // If amt given, just generate that much energy.
105 // Otherwise, assume click
106 generateEnergy(amt?:number) {
107 if(amt) {
108 this.energy += amt
109 } else {
110 this.energy += this.energyPerClick
111 }
112 },
113
114 addCircuit() {
115 if(this.canAffordCircuit) {
116 this.energy -= this.nextResourceCost("circuits")?.energy ?? 0
117 this.capacitors -= this.nextResourceCost("circuits")?.capacitors ?? 0
118 this.circuits++
119 }
120 },
121 addCapacitor() {
122 if(this.canAffordCapacitor) {
123 this.energy -= this.nextResourceCost("capacitors")?.energy ?? 0
124 this.capacitors++
125 }
126 }
127 }
128 }) // End of pinia store definition
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:
<script setup lang="ts">
import { gameStateStore } from '@/stores/gamestate';
const gameState = gameStateStore();
</script>
<template>
<div>
<button @click="gameState.generateEnergy()">Generate {{ gameState.energyPerClick }} Energy</button>
</div>
</template>
This creates a new 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 The template is defined in a 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 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?
script
tag, like this:<script setup lang="ts">
//...
</script>
template
tag, like this:<template>
//...
</template>
<script setup lang="ts">
//...
</script>
<template>
//...
</template>
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:
<script setup lang="ts">
import { gameStateStore } from '@/stores/gamestate';
const gameState = gameStateStore();
</script>
<template>
<div>
<p>Energy: {{ gameState.energy }}</p>
<p>Capacitors: {{ gameState.capacitors }}</p>
<p>Circuits: {{ gameState.circuits }}</p>
<p>Energy per click: {{ gameState.energyPerClick }}</p>
<p>Energy per second: {{ gameState.energyPerSecond }}</p>
<button @click="gameState.addCapacitor" :disabled="!gameState.canAffordCapacitor">Purchase Capacitor (
<span v-if="gameState.nextCapacitorCost.energy !== undefined">{{ gameState.nextCapacitorCost.energy }} Energy </span>
<span v-if="gameState.nextCapacitorCost.capacitors !== undefined">{{ gameState.nextCapacitorCost.capacitors }} Capacitors </span>
<span v-if="gameState.nextCapacitorCost.circuits !== undefined">{{ gameState.nextCapacitorCost.circuits }} Circuits </span>
)</button>
<br/>
<button @click="gameState.addCircuit" :disabled="!gameState.canAffordCircuit">Purchase Circuit (
<span v-if="gameState.nextCircuitCost.energy !== undefined">{{ gameState.nextCircuitCost.energy }} Energy </span>
<span v-if="gameState.nextCircuitCost.capacitors !== undefined">{{ gameState.nextCircuitCost.capacitors }} Capacitors </span>
<span v-if="gameState.nextCircuitCost.circuits !== undefined">{{ gameState.nextCircuitCost.circuits }} Circuits</span>
)</button>
</div>
</template>
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.
<button @click="gameState.addCircuit" :disabled="!gameState.canAffordCircuit">Purchase Circuit (
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:
<span v-if="gameState.nextCircuitCost.energy !== undefined">{{ gameState.nextCircuitCost.energy }} Energy </span>
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:
<span v-if="gameState.nextCircuitCost.capacitors !== undefined">{{ gameState.nextCircuitCost.capacitors }} Capacitors </span>
<span v-if="gameState.nextCircuitCost.circuits !== undefined">{{ gameState.nextCircuitCost.circuits }} Circuits</span>
)</button>
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:
<script setup lang="ts">
import Clicker from '@/components/Clicker.vue'
import StoreDisplay from '@/components/StoreDisplay.vue'
</script>
<template>
<div id="app">
<Clicker />
<StoreDisplay />
</div>
</template>
<style>
#app {
display: flex;
justify-content: center;
align-items: top;
height: 100vh;
}
#app > * {
margin: 1rem;
}
</style>
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:
npm run dev
VITE v4.3.9 ready in 360 ms
➜ Local: http://localhost:5174/
➜ Network: use --host to expose
➜ press h to show help
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
:
export const setInterval2 = (fn: VoidFunction, time: number) => {
// A place to store the timeout Id (later)
let timeout: any = null;
// calculate the time of the next execution
let nextAt = Date.now() + time;
const wrapper = () => {
// calculate the time of the next execution:
nextAt += time;
// set a timeout for the next execution time:
timeout = setTimeout(wrapper, nextAt - Date.now());
// execute the function:
return fn();
};
// Set the first timeout, kicking off the recursion:
timeout = setTimeout(wrapper, nextAt - Date.now());
// A way to stop all future executions:
const cancel = () => clearTimeout(timeout);
// Return an object with a way to halt executions:
return { cancel };
};
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:
1 <script setup lang="ts">
2 import Clicker from '@/components/Clicker.vue'
3 import StoreDisplay from '@/components/StoreDisplay.vue'
4 import { gameStateStore } from './stores/gamestate';
5 import { setInterval2 } from '@/helpers';
6 import { onMounted } from 'vue';
7
8 const gameState = gameStateStore();
9
10 onMounted( () => {
11 setInterval2(()=>{
12 gameState.energy += gameState.energyPerSecond * gameState.circuits
13 }, 1000);
14 });
15
16 </script>
17
18
19 <template>
20 <div id="app">
21 <Clicker/>
22 <StoreDisplay/>
23 </div>
24 </template>
25
26
27 <style>
28 #app {
29 display: flex;
30 justify-content: center;
31 align-items: top;
32
33 height: 100vh;
34 }
35
36 #app > * {
37 margin: 1rem;
38 }
39 </style>
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.
Have fun!
–Jesse