Hero image!

Some parts of a cookie clicker clone in Godot

October 23, 2023 - Drygast
Basics Game Dev GDScript Godot

To reiterate from my previous godot related post (breakout). I'm no expert with the Godot Engine; I'm just documenting my journey as I learn here. My experience is mostly trial-and-error with lots of research along the way. Through this "log", I hope to share what I've learned so far and provide a starting point for others who may be in a similar situation.

If you want to try a HTML version of this project before reading all of this, you can find it HERE.

To see all code and download the project, take a look in my Azure repo HERE, but I'll copy a few things here.

NOTE! I never completed this project with all the upgrade features for the clicker and other things that would make a fun game. It is just the most basic parts that are done.

A couple of new things

I'm still learning and I felt like I was missing a few things that I thought could add functionality to the project. So I started looking into stuff like singletons, loggers and config files.

Singletons

As far as I can understand, GDScript does not support singletons in the way that I thought. But there is a "sort of singleton"-way to do it. It involves autoloading a class during the init of the project and makes that file accessible to other files. It does NOT limit the instances to only one though, so it is possible to create numerous global objects. So technically it's not a singleton, but a good enough workaround.

First create the global variable in the file globals.gd

extends Node

# The current score of the player
var playerScore : int = 0

Next we must add the file to the autoload project settings.

Autoload

After this you can access the variable in main.gd like this:

extends Node

class_name Main

func _ready() -> void:
	Globals.playerScore += 15
	print(Globals.playerScore)

I think this is good enough and solved a few issues I hade with the previos project.

Logger

Next I wanted to have a better way to log things that happen than just print(). I searched for a short while and came across this one: https://www.nightquestgames.com/logger-in-gdscript-for-better-debugging/. I thought it was just the correct level of easy vs. function so I implemented it pretty much as that article describes. I did change a few minor things, but basically used the same concept. Thank you Night Quest Games for a great article and the code!

After adding the file to the autoload project settings, I could log messages as expected. Since this is autoloaded, I could not use it to log som of the _init() functions, but thats OK - I dont really need it except for when trying to figure out in what order the files are loaded. This is a test from the main.gd file:

func _init() -> void:
	print("Main:_init()")
	if (OS.is_debug_build()):
		Logger.SetLogLevel(Logger.EMessageSeverity.Debug)
	else:
		Logger.SetLogLevel(Logger.EMessageSeverity.Info)
	Logger.SetLogsFolder("res://logs")
	var status = Logger.CreateLogFile()
	if (status != OK):
		Logger.LogError("Unable to create log file [ Error code: %d ]" % status)
	Logger.LogDebug("Main:_init() - done")

func _ready() -> void:
	Logger.LogDebug("Main:_ready()")
	Logger.LogInfo("Current Score: " + str(Globals.GetPlayerScore()))
	Logger.LogDebug("Main:_ready() - done")

And this is the result in files:

Logfiles

Result in debug console:

Logentries in debug console

Config / Settings

Next - I just wanted to figure out a way to save settings. At first I thought I would actually need it, but as I continued to develop the project I realised that I did not. So basically I coded the thing, but never used it. I found it interesting though, so I decsided to keep it for future references. I also found out that it did not work as expected when exporting to HTML5 so I guess there is a few more things to figure out in the future.

Using the official documentation, I finally ended up with this config.gd file:

extends Node

## This class is autoloaded as Config and [i]ACTS[/i] like a singleton.
##
## Usage:
## [codeblock]
## Config.SetAudioEnabled(false)
## print("Audio Enabled: " + str(Config.GetAudioEnabled()))
## [/codeblock]

const SETTINGS_FILE_PATH = "res://settings.cfg"

var _audioEnabled : bool = true

func SetAudioEnabled(enabled : bool) -> void:
	_audioEnabled = enabled

func GetAudioEnabled() -> bool:
	return _audioEnabled

func LoadConfig() -> bool:
	var configFile = ConfigFile.new()
	var err = configFile.load(SETTINGS_FILE_PATH)
	if err != OK:
		configFile.save(SETTINGS_FILE_PATH)
		err = configFile.load(SETTINGS_FILE_PATH)
		if err != OK:
			return false
		else:
			err = configFile.load(SETTINGS_FILE_PATH)
			if err != OK:
				return false
	
	if (configFile.has_section_key("AUDIO_SECTION", "AUDIO_ENABLED")):
		_audioEnabled = configFile.get_value("AUDIO_SECTION", "AUDIO_ENABLED")
	else:
		SaveConfig()

	return true

func SaveConfig() -> bool:
	var newConfig = ConfigFile.new()
	var err = newConfig.load(SETTINGS_FILE_PATH)
	if err != OK:
		return false
	
	newConfig.set_value("AUDIO_SECTION", "AUDIO_ENABLED", _audioEnabled)
	newConfig.save(SETTINGS_FILE_PATH)
	return true

To use it you have to add the file to the autoload project setting, and then use it through this code:

	Config.SetAudioEnabled(false)
	print("Audio Enabled: " + str(Config.GetAudioEnabled()))

I placed an empty settings.cfg file in the root of the tree.

Settings file in tree

And here is the content of the file after I used the SaveConfig function.

Settings file content
 

Really large numbers

OK, so it was time to actuelly get working on the game itself. I wanted to do at least a few buildings and upgrades to capture som basic functionality of the idle-game concept. The original game has grown a lot since it was first created and I never intended to make a full clone or anything, but rather have some familiar game concepts in the background when I learned more Godot stuff.

The first problem was that the nymbers in that type of game eventually gets so large that there is no existing type that can handle them. That led me to create a custom class that I called MassiveNumber. This class is far from perfect - in fact it might not even be good, but it worked for this application so I decided to keep it even though it was far from pretty. I'm not going to paste every row of code here, so if you are interested in telling me about all things I did wrong you have to go to the Azure repo.

The basic principle I used was to keep multiple integers (0-999) in an array and add/remove the next position in the array if the number was larger than a single array should handle.

The class supports up to Vigintillion (only add and subtract though) so it should be more than sufficient to handle my basic click-game. I also noticed that GDScript does not support operator overloads so I had to use Add and Subtract functions instead. Here is a sneak peak into the massive_number.gd file:

# Message severity names.
var _massiveNumberNames : Array[String] = ["Decimal", "Hundred", "Thousand", "Million",
						"Billion", "Trillion", "Quadrillion", "Quintillion",
						"Sextillion", "Septillion", "Octillion", "Nonillion",
						"Decillion", "Undecillion", "Duodecillion", "Tredecillion",
						"Quattuordecillion", "Quindecillion", "Sexdecillion", "Septendecillion",
						"Octodecillion", "Novemdecillion", "Vigintillion"]

# The actual value of the different types. Together they define the complete number.
# 0, 1, 2, 3,			= 3002001.000		3.00 Million
# 999, 999, 999, 999,	= 999999999.999		999.99 Million
var _massiveNumberValues : Array[int] = [0, 0, 0, 0,
						0, 0, 0, 0,
						0, 0, 0, 0,
						0, 0, 0, 0,
						0, 0, 0, 0,
						0, 0, 0]

Buildings

I needed a few buildings just to test how to buy and increase the level of said building. For this I created a basic dictionary filled with arrays with the data for every building. I could not find another way to create multidimensional arrays, so this would have to suffice.

Here is a preview of how that looked:

	buildingDictionary = { 
		# DictionaryIndex	"Name",		"Effect",				level	max		baseCost	...
		"Cursor":			["Cursor",	"Autoclicks every 10s",	0,		5059,	15, 		...
		"Grandma":			["Grandma",	"1/s",					0,		5045,	100,		...
		"Farm":				["Farm",	"8/s",					0,		5028,	1100,		...
        ...

I kept this dictinary in the globals.gd file so that I could access the data from multiple places in the code.

Next I needed images for all the buildings. I'm not very good with graphics, so I used an AI to generate the icons for me. I had to run it a bunch of times and then modify the images to have a specific size and also to support transparent background. I think they turned out pretty OK, at least for this type of development project / prototype.

Click mask

With basic placement of the graphics into multiple scenes, it was time to add the actual kanelbulle that would be clicked to start everything. I used the TextureButton base class for this and added my texture from file.

There was a small problem though - my kanelbulle was round and the click reacted to things in the corner of the image that looked strange. I solved it by using a click mask. It involved creating a black and white version of my image and importing it as a bitmap. I tried using it directly from the folder of images, but got the "must be a bitmap" error. To change this I had to go to the import tab for that image and chenge it to be imported as bitmap.

Import file as bitmap

After that, I could use it as a mask for my button and all clicks in the black area was now ignored.

Click mask

So much more...

I feel like this article is quite long already and dont really want to fill it with just copies of my code. Let's just say that there was a few more things to figure out, but the things I listed above are the ones that I found to be new and kind of interesting. If you really want to dig into the code more, I have links in the beginning of this article to the Azure repo where the entire project is available to download.

I ended up at a point where the game could be played pretty well, but without the upgrades to the cursor and clicking the kanelbulle with the mouse, it is definitely missing some key components. But again - my goal with this project was not to build a full clone, I only wanted to learn more about Godot.

Final thoughts

I've learned even more now. I find myself using different code style all the time and I guess that is just a matter of time since I figure out what works best for me. I also have issues with what is the best way to implement different things as I hardly know the best prectices for GDScript or Godot in general. I think this is just a matter of time as well and soon I figure out a proper style of coding, but I'm not there yet. Anyway - it was fun to build this project even if I probably did a bunch of things in a suboptimal way. The end result was a fun base for a idle-game, but I'm not going to continue development here - it's just a base for learning Godot.