Godot Tactics RPG – 02. Board Generator


Hey, its me 7thSage again, welcome back to part 2 of the Godot Tactics series. This week we’ll be dipping our toes into the first bit of Gdscript for the project. We’ll create everything we need to build our tiles and generate a board or two.

Gdscript

Before we get into any code, we need to take a quick look at some syntax. Gdscript is similar to Python. You won’t see many curly braces or semi-colons. Loops are separated based on indentation. And if you have a side in the war of spaces vs tabs, I’m sorry to say, or perhaps happy to say depending on the side you’ve chosen, but Gdscript requires Tabs. Now for the unfortunate news, code display on the web isn’t always straight forward. If you want to copy code, click the button in the corner of any snippet Toggle RAW Code. This will give you the non syntax highlighted version of code that still has Tabs intact. If you try to copy the syntax highlighted code, you will need to convert the spaces into tabs yourself.

Comments are similar to Python again. Anything after a # sign is a comment. There are no multi-line comments, but some people get around it by using multi-line strings that are not assigned to a variable, three quote marks at the beginning and end of the text. “”” this is a comment “””. Although it looks like this can only be used in certain parts of your scripts. Probably the best option for multi-line comments is to write all the lines, then select all of them and right-click on the text and choose toggle comments.

# this is a comment

""" this is basically 
a comment """

If you’ve programmed in c# or another language before, you probably know enough about loops and variables to kinda figure them out in Gdscript without too much effort so I won’t go into detail about that here. If you’ve never programmed before, this might be a good time to go over a quick introduction someplace, either a beginner Godot 4.x scripting course, or it may be easier for the time being to find a beginner Python course. Just know that there will be some differences in the languages, but I’ll be showing all the code I use, so if there is ever any confusion, you can compare your code.

Point Struct

In the original tutorial this held our x and y(x and z technically?) map coordinates. Because we don’t want in between values, a standard Vector2 won’t work, since that uses floats, and we are looking for something with int. In the version of Unity the original tutorial was created, there was no such thing as an Int Vector, but in later versions of Unity, and in Godot, we actually already have something that fits that bill, so we’re going to just use what is there. Vector2i. (I think Unity introduced Vector2Int in the 2018 version). So that’s all for this section.

Tile Script

We’ll create two folders here. “View Model Component” and “Model”, which we’ll use later. These are based on the architecture pattern “MVC”, which stands for “Model View Controller”. The general idea is that the data itself is modified and stored separate from where the code to format it on screen is kept. Hit Point data doesn’t care what font it is, nor the color or size. For the folder “View Model Component”, in the original tutorial the reasoning behind the name is as follows:

Not everything fits well into the “MVC” architectural pattern. Sometimes it makes more sense to blur the lines between the model and view. Actually there is another pattern called “MVVM” which stands for Model-View-ViewModel which might feel a bit more aligned with our next script. To be most correct I am just creating a “Component” which is a great architectural pattern all by itself, but because I can have controllers that are components etc, it feels a little confusing to organize by that word.

Create two subfolders under scripts named “Model” and “View Model Component”. Right click on the “View Model Component” folder and create new script. Leave the Language as GDScript. The other options we’ll leave as default. Under path, change the name of the script at the end to “Tile.gd”. Once done, press Create at the bottom to finish creating the script.

When first opening the script we’ll see a few things by default. “extends Node” is the class the script inherits from, much like monobehaviour in Unity. The two functions _read() and _process(delta) are similar to Awake() and Update() in Unity. Inside the functions is a tab indenting the line, then the word pass. Godot will throw an error if a function is completely empty, so any functions we create as placeholders will need it there.

For now you can delete everything except “extends Node”

We need a few things at the beginning of the code. Because we are going to be updating and viewing the tiles in the editor, we need the @tool command at the top. We’ll add the same thing to the top of the board creator later, but since this will be accessed by code running in the editor, it needs the flag. If it is not in the tile code, the board editor will be able to tell the script is attached and what methods it has, but it won’t be able to access them when it comes time to run said methods.

class_name is optional for scripts, but when we give it a name, we can compare node types with it.

@tool
extends Node
class_name Tile

Next we’ll create a const for the how tall each tile step will be. Unlike Minecraft, each block will not be a completely square cube, but instead something with smaller steps. We do this as a const so we have a central place we can change the value if we decide later on that something else looks better, and only need to change it one place. It also eliminates a “magic number” in our code. In six months, “stepHeight” will make more sense than seeing “0.25” somewhere in our code.

const stepHeight: float = 0.25

The next two values let the tile itself keep track of its position. pos is the tile’s x and z coordinates, while the y is handled with height

var pos: Vector2i
var height: int

center() is a convenience function that lets us get the value of the top edge of a block. Useful for placing objects and characters on the surface of a tile.

func center() ->Vector3:
	return Vector3(pos.x, height * stepHeight, pos.y)

Anytime a tile is modified, we’ll need to call this to set its scale to the correct height. First we scale the tile to the correct height, and then because the pivot point is at the center of the tile instead of the bottom, we need to actually move the tile up half the height to get it in the correct place.

func Match():
	self.scale = Vector3(1, height * stepHeight, 1)
	self.position = Vector3(pos.x, height * stepHeight /2.0, pos.y)

The board will be created by growing and shrinking tiles. There is no logic here for capping the max and min height, we’ll be taking care of that in the board creator script. Whenever the tile is updated however, we update the scale with Match().

func Grow():
	height += 1
	Match()

func Shrink():
	height -= 1
	Match()

Last up we create a function to set a specific height and position of a tile. In c# we would be able to overload the function to use multiple signatures, but GDScript does not support that, so we’ll have to use two different names for each function, “Load()” and “LoadVector()”

func Load(p: Vector2i, h: int):
	pos = p
	height = h
	Match()

func LoadVector(v: Vector3):
	Load(Vector2i(v.x,v.z),v.y)

Now that we are finished with that script, we need to attach it to our Tile prefab. Open the Tile scene, and click on the “Tile” node. In the inspector under node you will see a value for Script. Click on the the dropdown and choose “Load”. Find the Tile.gd script in the explorer and add it. Alternately you can drag the Tile.gd script from the FileSystem pane onto the Tile node. Be sure to save the scene.

Board Creator Scene

Under the scripts folder create a subfolder called “PreProduction”. We’ll put scripts in here that help us create things for the game, but aren’t part of the game itself. Inside the folder create a new script “BoardCreator.gd”

Next, under the “Scenes” folder, create a new 3D Scene and name it “BoardCreator.tscn”, the extension is added with the dropdown, so you don’t need to type that. Once the scene is created, Open it and drag the “BoardCreator.gd” script onto the root “BoardCreator” node, or alternatively, select the node and in the inspector select the Load option under the Script property.

BoardCreator Stub

We’ll come back to the board creator class in more detail later, but for now lets lay some foundations the rest of the code will be accessing.

We’ll leave alone the “extends Node” and the _ready() and _process() methods and to the top of the script add @tool and a class name. We’ll need @tool so the code can be accessed by objects in the editor window, and we’ll need that name so we can check its type later.

@tool
class_name BoardCreator

Next we’ll add some empty functions that we’ll be calling from the plugin we create next. There are two separate versions of save and load. You’ll be able to choose the one you like and ignore or delete the other one. For testing purposes, we’ll start with a simple print statement in each function. Alternately we could add the keyword “pass” to have the functions do nothing. You’ll need one or the other though, because the function will error if it is completely blank.

func Clear():
	print("Clear Pressed")

func Grow():
	print("Grow Pressed")

func Shrink():
	print("Shrink Pressed")

func GrowArea():
	print("GrowArea Pressed")

func ShrinkArea():
	print("ShrinkArea Pressed")

func Save():
	print("Save Pressed")

func Load():
	print("Load Pressed")

func SaveJSON():
	print("SaveJSON Pressed")

func LoadJSON():
	print("LoadJSON Pressed")

Board Creator Plugin

In order to modify the Godot interface, we’re going to need to create a simple plugin, and we’ll only need a couple short Gdscripts to do it. No need to jump into c++ or c# for this. And no need to recompile the engine or anything like that.

In the top left corner under the “Project” menu select “Project Settings”.

In the Project Settings window, select the “Plugins” tab and click “Create New Plugin”.

In the dialog that opens up, enter “BoardCreatorInspector” in both Plugin Name and Subfolder. Enter a name under Author. Language will be GDScript and finally script name we’ll use “plugin.gd”. This will be the entry point for our plugin. We’ll create one more script in a minute that will contain the majority of the plugin’s code.

Be sure to type the plugin name “plugin.gd” into the script name field. Even though it looks like the default would be “plugin.gd”, it will actually default to use the same name as the Subfolder.

Once you click create, A folder will be created in the addons folder with our plugin name, and inside will be a “plugin.cfg” file, which contains the info we listed in the form, and the “plugin.gd” file. Inside the same folder create one more script named, “BoardCreatorInspector.gd”. This will be the code for our buttons.

The plugin.cfg file may take a few moments before it becomes visible in the engine’s file explorer. If you rename the main plugin script, you will also need to update the name inside plugin.cfg as well.

Open the plugin.gd file. @tool allows this to work in the editor, and you’ll notice that this time instead of extends node, we’re using extends EditorPlugin. First we need a variable to store the plugin so we can load and unload it. Under _enter_tree() we’ll preload the plugin and then call add_inspector_plugin(plugin) to load it. Then we will free it when we are done in the _exit_tree with remove_inspector_plugin(plugin)

@tool
extends EditorPlugin

var plugin

func _enter_tree():
	plugin = preload("res://addons/BoardCreatorInspector/BoardCreatorInspector.gd").new()
	add_inspector_plugin(plugin)

func _exit_tree():
	remove_inspector_plugin(plugin)

Main plugin script

Open “BoardCreatorInspector.gd”

@tool again because we’re going to be using this in the editor, however this time instead of extending node, we’re using “extends EditorInspectorPlugin” because this plugin is modifying the inspector.

@tool
extends EditorInspectorPlugin

This bit is where you tell the plugin where in the editor to show the plugin. If we just return true, it will show on all objects. Here we only show the plugin if the object is a BoardCreator, which is one of the reasons we named the class earlier.

func _can_handle(object):
	if object is BoardCreator:
		return true
	return false

Here is where we’ll put everything that we want to show. In this case we’re creating several buttons, one for each of the stub classes we created earlier. We create the button with Button.new(), then set the buttons label with .set_text, then we connect the button press event to the method with pressed.connect, and finally we load the button we created into the inspector.

func _parse_begin(object):
	var btn_clear = Button.new()
	btn_clear.set_text("Clear")
	btn_clear.pressed.connect(object.Clear)
	add_custom_control(btn_clear)

	var btn_grow = Button.new()
	btn_grow.set_text("Grow")
	btn_grow.pressed.connect(object.Grow)
	add_custom_control(btn_grow)

	var btn_shrink = Button.new()
	btn_shrink.set_text("Shrink")
	btn_shrink.pressed.connect(object.Shrink)
	add_custom_control(btn_shrink)

	var btn_growArea = Button.new()
	btn_growArea.set_text("Grow Area")
	btn_growArea.pressed.connect(object.GrowArea)
	add_custom_control(btn_growArea)
	
	var btn_shrinkArea = Button.new()
	btn_shrinkArea.set_text("Shrink Area")
	btn_shrinkArea.pressed.connect(object.ShrinkArea)
	add_custom_control(btn_shrinkArea)
	
	var btn_save = Button.new()
	btn_save.set_text("Save")
	btn_save.pressed.connect(object.Save)
	add_custom_control(btn_save)
	
	var btn_load = Button.new()
	btn_load.set_text("Load")
	btn_load.pressed.connect(object.Load)
	add_custom_control(btn_load)

	var btn_saveJSON = Button.new()
	btn_saveJSON.set_text("Save JSON")
	btn_saveJSON.pressed.connect(object.SaveJSON)
	add_custom_control(btn_saveJSON)
	
	var btn_loadJSON = Button.new()
	btn_loadJSON.set_text("Load JSON")
	btn_loadJSON.pressed.connect(object.LoadJSON)
	add_custom_control(btn_loadJSON)

And that wraps up the plugin. If we click on BoardCreator in the 3D scene, we should see our buttons in the inspector. If we don’t, be sure your scripts and scenes are all saved, and you may need to open and close the scene file, or deactivate/activate the plugin inside project settings where we created it. If there are errors in any of the scripts, you may have to fix them before it allows you to enable the plugin.

The buttons don’t do anything much yet, but they are all wired up to connect to the methods we created earlier. Be sure to give them all a quick test to make sure they are all working and wired to the right functions. There is console window at the bottom center of the editor. You might have to click the “Output” button to bring it up.

Back to BoardCreator.gd

Lets start off by setting some variables for how large the world is. width, depth and height. The @export command exposes the variable in the inspector, similar to [SerializeField] in Unity. _oldPos will be a variable to help us keep track of when we type in a new value for pos in the inspector. tiles = {} will be our dictionary holding the tile values for our map.

@export var width: int = 10
@export var depth: int = 10
@export var height: int = 8
@export var pos: Vector2i
var _oldPos: Vector2i
var tiles = {}

The next lines are where we store a reference to our tile and selection indicator prefabs. My initial thought was to add @export to expose these in the editor, but I found it less useful because you can’t drag a prefab into the inspector to set it. You can however drag things from the FileSystem pane to the code editor directly, and it will paste the path to that object into the code, so go ahead and do that for the paths of the tileViewPrefab and the tileSelectionInditorPrefab. marker we will use shortly to hold a reference to the instantiated tile prefab.

var tileViewPrefab = preload("res://Prefabs/Tile.tscn")
var tileSelectionIndicatorPrefab = preload("res://Prefabs/Tile Selection Indicator.tscn")
var marker

There are a few more lines we need to add to this section later, but for now lets move on to the _ready() method. This is basically the Awake() method in Unity. Here we are loading the tileSelectionIndicatorPrefab into the scene with .instantiate(), and saving it to the variable marker, that we created earlier. Then we add that as a child to our BoardCreator. The last two lines we are setting the initial value of our tile selector, and we’ll set match the _oldPos up with it to start off.

As a quick note, we won’t see the children that we instantiate in the Scenetree until we hit play. You’ll be able to access the entire tree under the remote tab in the scenetree window, however, we don’t have our camera set up yet, so you won’t see your terrain quite yet if you are in play mode.

func _ready():
	marker = tileSelectionIndicatorPrefab.instantiate()
	add_child(marker)
	
	pos = Vector2i(0,0)
	_oldPos = pos

Another quick note before we go on. Gdscript does not have private variables. The common convention to work around this is to prefix variables or methods that we would normally consider private with an underscore like “_oldPos”. While this does not actually make them private, it gives a visual indicator of what shouldn’t be touched outside the class. So be aware that if you spot the convention someplace, you should at least think twice before using the variable or method from outside the given class if it uses the convention.

Now lets fill in the default _process() method, the Godot equivalent of Unity’s Update(). Here we check if the value of pos is the same as _oldPos. If it isn’t, we’re going to set them to be the same and update the location of our tile selector. We’ll only be updating the marker in this class.

func _process(delta):
	if pos != _oldPos:
		_oldPos = pos
		_UpdateMarker()

Lets create a new func _UpdateMarker(). This will check if the tiles dictionary has the current pos and if it does, set the cursor to the top of the tile. If the dictionary does not contain the value, it will set the position at that x,z coordinate and set y to the floor, zero.

func _UpdateMarker():
	if tiles.has(pos):
		var t: Tile = tiles[pos]
		marker.position = t.center()
	else:
		marker.position = Vector3(pos.x, 0, pos.y)

Now we’ll start building out the methods for our buttons. The first one is fairly straight forward. Clear(). We can start by deleting the print() call. Now that we’re adding actual code we won’t need them for visual indication. We loop through the entries in the dictionary and free any geometry created. Then we clear the values in the dictionary. Godot does not have auto garbage collect, so we need to free the objects when we are done with them. However, if we delete a node, Godot will take care of freeing up the children. With this bit of code, our first button officially works as intended, but since we currently have no data to delete, we won’t be able to see anything quite yet.

func Clear():
	for key in tiles:
		tiles[key].free()
	tiles.clear()

Next we start to go down a bit of a rabbit hole. We’ll need to create a couple sets of methods before we get the next couple buttons working. The code here is pretty simple, we use the cursor “pos” variable to determine where to create a tile and call the respective _GrowSingle() or _ShrinkSingle().

func Grow():
	_GrowSingle(pos)

func Shrink():
	_ShrinkSingle(pos)

We continue down the rabbit hole. In grow single we introduce _GetOrCreate(). This will grab either the current tile, or create a new one. With a reference to the tile the cursor is at, we check if its at the maps max height, and if it isn’t, we call the tiles .Grow function, and call _UpdateMarker.

_ShrinkSingle does nothing if there isn’t a tile already present, and if there is a tile it shrinks it down. Finally we check if the tile’s height is less than or equal to zero, we delete the tile geometry and the entry in the dictionary.

func _GrowSingle(p: Vector2i):
	var t: Tile = _GetOrCreate(p)
	if t.height < height:
		t.Grow()
		_UpdateMarker()

func _ShrinkSingle(p: Vector2i):
	if not tiles.has(p):
		return
	
	var t: Tile = tiles[p]
	t.Shrink()
	_UpdateMarker()
	
	if t.height <= 0:
		tiles.erase(p)
		t.free()

This is the last of the functions we’ll need for Grow() and Shrink(). If _GetOrCreate() finds a tile in the dictionary, it returns that to use, otherwise it creates a new Tile and sets it’s values to the current position and returns. _Create() instantiates a tile and returns it for _GetOrCreate() to use.

func _GetOrCreate(p: Vector2i):
	if tiles.has(p):
		return tiles[p]
	
	var t: Tile = _Create()
	t.Load(p, 0)
	tiles[p] = t
	
	return t

func _Create():
	var instance = tileViewPrefab.instantiate()
	add_child(instance)
	return instance

With that, our first three buttons should be working. You can manually input a “pos” value in the inspector since we exposed that variable, and when we press grow or shrink a tile should grow or shrink at that location when we press it.

For the next set, lets go the other direction and lay out the groundwork before creating the functions. We’ll start by going back towards the top of the script where the variables are and create a new randomNumberGenerator. We’ll use this to get random rectangles to grow and shrink for our GrowArea and ShrinkArea methods.

var _random = RandomNumberGenerator.new()

Before using a random number generator its a good idea to set a seed to get things started, so that we aren’t generating the same list each time. In the “_ready()” function, add random.randomize() to get a seed value from the computer’s clock. Since we are only calling this once, its ok that the same second would be generating the same value each time. In game we wouldn’t want to call or set a seed like this each time we roll, as we may have multiple rolls every second which would result in them all returning the same result. Luckily we don’t need to set the seed that often anyway, so it really isn’t an issue.

	_random.randomize()

Now we can create the rectangle that we’ll use in next step. Rect here works the same as Unity. We set one corner, and then give it a width and height value. However, that being said, random int range works differently. In Unity the range is (inclusive, exclusive) when dealing with ints, and in Godot it is (inclusive, inclusive). Another difference is that Godot has a second rect that uses int values. Rect2i.

func _RandomRect():
	var x = _random.randi_range(0, width - 1)
	var y = _random.randi_range(0, depth - 1)
	var w = _random.randi_range(1, width - x)
	var h = _random.randi_range(1, depth - y)
	return Rect2i ( x, y, w, h )

Now back to the buttons, we’ll have one more set of methods to write before they work, but the buttons themselves are pretty simple. We generate a random rectangle, and we call a method with it to grow or shrink the board respectively.

func GrowArea():
	var r: Rect2i = _RandomRect()
	_GrowRect(r)

func ShrinkArea():
	var r: Rect2i = _RandomRect()
	_ShrinkRect(r)

The last two methods we need for these buttons. We loop through each rect and call _GrowSingle and _ShrinkSingle respectively on each tile within the bounds.

func _GrowRect(rect: Rect2i):
	for y in range(rect.position.y,rect.end.y):
		for x in range(rect.position.x,rect.end.x):
			var p = Vector2i(x,y)
			_GrowSingle(p)

func _ShrinkRect(rect: Rect2i):
	for y in range(rect.position.y,rect.end.y):
		for x in range(rect.position.x,rect.end.x):
			var p = Vector2i(x,y)
			_ShrinkSingle(p)

And with that, we have everything we need to start creating some boards. All that is left is saving and loading.

Saving and Loading

Lets start by creating a new folder under Data called “Levels”

Before I get into saving and loading I think I need to talk a little bit about our options. In general I think there are three main options. We can load a resource file, which I think is somewhat similar to a scriptable object, although there is some talk about it being unsafe, although I’m not sure if that has been fixed. The problem lies in that resource files can include anything, including code. Because this is an editor script, and not player facing(as long as you aren’t providing it as a level editor) then it should be fine to use, but its probably best to avoid that for things like save files.

The next option is we can write and read data from a file binary. This is what I prefer, but its harder to read, and may get unwieldy for larger projects.

The third option is JSON. It’s a bit more complicated to write for simple data, but it keeps things more organized and is easier to read, which depending on the situation, could be a plus or minus. As I said before, because this isn’t player facing, we can really choose any of the options, but I’ll try to give an example of the last two.

Before we get into the actual code though, there is one last thing I want to mention that we have a choice on. As I mentioned earlier, the FileSystem Panel in the editor only shows certain data types, and ignores anything else. We can still use them in our code, but we won’t be able to see them in our editor. So the question becomes, what extension do we use for our map files? .json works well, assuming we are using JSON, but if we are using binary files, there isn’t really anything that works perfectly. we can use one of the Godot specific formats, but I worry about confusing it with a different type of asset the program uses. So you can choose whichever format you think works best, and while its far from perfect, I went with using “.txt”. While binary isn’t exactly text, it at least won’t be confused with other Godot files.

Now lets head back up to the top of BoardCreator.gd where the variables are and we’ll add a couple for our save directory. We can drag the folder from the FileSystem pane onto the script window to type out the path for us. As for filename, give it whatever name you want to save your map as. We added @export here as well, so we can modify it in the inspector, but remember, if it doesn’t have an extension Godot recognizes, it won’t be visible in the FileSystem panel.

var savePath = "res://Data/Levels/"
@export var fileName = "defaultMap.txt"

Because fileName can be modified in the editor, I save it for here to combine the savePath and fileName, otherwise we could be saving to an outdated name and its not necessary to keep track of it changing since we aren’t using it until this point anyway. After that, we open the file, giving it WRITE access. store_8 creates a single byte, 8 bits in the binary. The first thing we store is a version number for the save format. If we change the format in the future and have a lot of maps already premade, this lets us add logic to load the old maps. For instance the current maps don’t have terrain type, but we add it later. While there is no terrain info in map version 1, we can set logic to just load all tiles as dirt or grass to make the map work. The next value we store is the number of tiles. We need 16 bits for the number of tiles in case we ever go above a 16 x 16 map. Right now we could just load until the end of the file and it would work, but if we add more data to our map format, we may need to stop loading tiles before the end of the file and load something else. This lets us know when. Finally we store the map values the point values(technically x,z) and height by looping through each tile and grabbing the values. When we are done we close the file.

func Save():
	var saveFile = savePath + fileName
	var save_game = FileAccess.open(saveFile, FileAccess.WRITE)
	var version = 1
	var size = tiles.size()
	
	save_game.store_8(version)
	save_game.store_16(size)

	for key in tiles:
		save_game.store_8(key.x)
		save_game.store_8(key.y)
		save_game.store_8(tiles[key].height)
		
	save_game.close()

Loading is pretty similar to saving, but there are a couple extra steps. First we need to clear the board so we don’t end up with duplicate tiles, or tiles from the old map that shouldn’t be in the new one. When saving it doesn’t matter if the file doesn’t exist yet, but when loading we need to check to make sure there is a file first. Then we do what we did previously in the same order. We grab the version number, number of tiles and loop though. Each tile we loop through we create a new tile, and load in its position and finally save it to the dictionary. Then close the file and update the marker.

func Load():
	Clear()
	
	var saveFile = savePath + fileName
	if not FileAccess.file_exists(saveFile):
		return # Error! We don't have a save to load.
		
	var save_game = FileAccess.open(saveFile, FileAccess.READ)
	var version = save_game.get_8()
	var size = save_game.get_16()
	
	for i in range(size):
		var save_x = save_game.get_8()
		var save_z = save_game.get_8()
		var save_height = save_game.get_8()
		
		var t: Tile = _Create()
		t.Load(Vector2i(save_x, save_z) , save_height)
		tiles[Vector2i(t.pos.x,t.pos.y)] = t	
	
	save_game.close()
	_UpdateMarker()

Saving and Loading JSON

The first step in creating our JSON file will be to create a dictionary set up in the way we want our file. Quick note: Dictionaries are denoted with two {} and arrays are denoted with []. We create an array for tiles instead instead of a dictionary so we aren’t required to add a separate key value for each tile. Once the dictionary is set up, we open the file with Write access like before. This time I have the value hard coded just to keep the two save files separate, but if JSON is your preferred method, I’d choose to replace the file with the same variable setup from the original. I left it commented out in the example below, just comment out the current line and uncomment the two previous lines. The actual saving portion is pretty short. First convert the dictionary to a JSON string, then send it to the file.

func SaveJSON():
	var main_dict = {
		"version": "1.0.0",
		"tiles": []
	}
		
	for key in tiles:
		var save_dict = {
			"pos_x" : tiles[key].pos.x,
			"pos_z" : tiles[key].pos.y,
			"height" : tiles[key].height
			}
		main_dict["tiles"].append(save_dict)

	#var saveFile = savePath + fileName
	#var save_game = FileAccess.open(saveFile, FileAccess.WRITE)
	var save_game = FileAccess.open("res://Data/Levels/savegame.json", FileAccess.WRITE)
	
	var json_string = JSON.stringify(main_dict, "\t", false)
	save_game.store_line(json_string)

Loading JSON is similar to before. We need to clear the map, then make sure the file exists. Again I commented out the lines to load the file with the variable we set in the inspector. Once the file is opened we load in the file as text, create a JSON object, and parse the text into it. We do a final check on whether the data is correct. Then we create a dictionary to put the data into, and then transfer the data from JSON into it. Once the data is in a dictionary, we need to load it to our map. We loop through everything in the tiles array. Creating a tile object and loading in the values for it. And just like before add it to the map’s dictionary. And with that we’re done loading. Close the file and update the tile marker.

func LoadJSON():
	Clear()

	#var saveFile = savePath + fileName
	#if not FileAccess.file_exists(saveFile):
	if not FileAccess.file_exists("res://Data/Levels/savegame.json"):
		return # Error! We don't have a save to load.
	
	#var save_game = FileAccess.open(saveFile, FileAccess.READ)	
	var save_game = FileAccess.open("res://Data/Levels/savegame.json", FileAccess.READ)	

	var json_text = save_game.get_as_text()	
	var json = JSON.new()
	var parse_result = json.parse(json_text)

	if parse_result != OK:
		print("Error %s reading json file." % parse_result)
		return
		
	var data = {}
	data = json.get_data()

	for mtile in data["tiles"]:
		var t: Tile = _Create()
		t.Load(Vector2(mtile["pos_x"], mtile["pos_z"]) , mtile["height"])
		tiles[Vector2i(t.pos.x,t.pos.y)] = t
	
	save_game.close()
	_UpdateMarker()

Common Problems

Remember to be careful with tabs and indentation. If things aren’t working the way you expect, double check that all your scripts and scenes are saved. If the changes are still not showing up, close and reopen the scene or if all else fails, Godot itself.

If none of that is working, be sure Tile.gd and BoardCreator.gd both have @tool and class_name in their scripts. Also be sure they are attached to their respective scenes. You’ll also need @tool in the plugin scripts. You may also need to make sure all your path variables are set to folder locations that exist. I didn’t add any specific code here to check whether the folder has been created to keep complexity down, and since this is not player facing code, I think that should be ok.

If you are still having trouble I’ve updated the repository with Part 2 so you can compare your version to mine. I’ve also added the icon from part 1 to the repo, since I forgot to add it while I was going through the final pass of the first lesson.

Summary

Well, everything should work now. We covered quite a bit in this lesson. Created our first plugin, and wired it up to create our first board or two. We also got the first bit of Gdscript under our belt.

There were a few Godot quirks in here that you can start to see some of the rough edges of the engine, but nothing too major, and I’ve seen discussion about most of the issues, so we should hopefully see those rough edges smoothed out in the coming versions.

6 thoughts on “Godot Tactics RPG – 02. Board Generator

    1. Sorry about that. Life has been pretty hectic with holidays and I’ve been helping my brother remodel a house for our mom. Next part shouldn’t be much longer now though, I just finished up the last couple details I wanted to add for it and the lesson after as well.

  1. while setting variables in BoardCreator.gd you say “My initial thought was to add @export to expose these in the editor, but I found it less useful because you can’t drag a prefab into the inspector to set it.” you can do this if you set the variable type as PackedScene

Leave a Reply

Your email address will not be published. Required fields are marked *