Disclaimer

My project uses autotiles and animated pieces, hence some code may seem a bit overkill for your use case.

img alt ><

You can support us through Patreon or get access to the final version of the code on GitHub

1. Drawing the board

First you will need to setup a autotiler in order to draw the board and have those nice and crispy edges. I used BetterTerrain to have more control of the TileMap and the Tiny Swords asset pack.

Then start populating the TileMap with respective cells

var map_shape = Vector2i(8, 8)

# set background of map
for col in range(map_shape.x):
	for row in range(map_shape.y):
		var map_cell = Vector2i(col, row)
		BetterTerrain.set_cell(self, DrawLayers.TERRAIN, map_cell, AutotilerLayers.GROUND)
		BetterTerrain.update_terrain_cell(self, DrawLayers.TERRAIN, map_cell)

You can also center the TileMap by doing

# center map
var viewport_center = get_viewport().size / 2
viewport_center -= (map_shape * tile_set.tile_size) / 2
position = viewport_center - Vector2i(0, rendering_quadrant_size/2)

2. Defining chess pieces

2.1. Defining Piece Constants

What I did here is that I first created a Constrants.gd preload file that I used to store information about chess pieces and also general stuff that should not change in-game.

You can then store information inside of it such as the sprites that will be used for each individual piece such as

var UNITS = {
	"pawn": {
		"sprites": {
			"head": preload("res://assets/units/components/Head/HeadPawn.png")
		}
	},
	"rook": {
		"sprites": {
			"head": preload("res://assets/units/components/Head/HeadRook.png")
		}
	},
	...

2.2. Creating a Piece Template

We can then create a Unit.tscn class that we’ll use as a template to create chess pieces. The structure should look something like this

Unit (Node2D) # root of the scene
|__ Sprites (Node2D) # in charge of applying changes on sprite components
	|__ LeftArm (Sprite2D) 
	|__ Body (Sprite2D)
	|__ Head (Sprite2D) 
	|__ RightArm (Sprite2D) 
|__ AnimationPlayer (AnimationPlayer)

Then we add some code to the Unit node

extends Node2D

@onready var sprites = $Sprites

var unit_type = "pawn"
var unit_side = "blue" # sides are blue (white) and red (black)

func init(unit_info):
	unit_type = unit_info["unit_type"]
	unit_side = unit_info["unit_side"]

func _ready():
	sprites.update_sprites(
		Constants.UNITS[unit_type]["sprites"],
		Constants.PALETTES[unit_side]
	)

2.3. Changing Colors using Shaders

You may have seen that I’m calling the Sprites node with a extra variable called Constants.PALETTES. That’s because I swap the palettes of a sprite at run-time to make blue pieces and red pieces.

I’ve added the following shader to the Sprites node and told all it’s children to use the parent material

shader_type canvas_item;

uniform vec4 old_main_color : source_color;
uniform vec4 old_shadow_color: source_color;
uniform vec4 old_light_color: source_color;

uniform vec4 new_main_color : source_color;
uniform vec4 new_shadow_color: source_color;
uniform vec4 new_light_color: source_color;

void fragment() {
    vec4 current_pixel = texture(TEXTURE, UV);

    if (current_pixel == old_main_color) {
		COLOR = new_main_color;
	}
        
	if (current_pixel == old_shadow_color) {
		COLOR = new_shadow_color;
	}
        
	if (current_pixel == old_light_color) {
		COLOR = new_light_color;
	}
}

You can then change the colors programmatically inside the Sprites node by doing

func update_palettes():
	var default_palette = Constants.DEFAULT_PALETTE
	
	material.set_shader_parameter("old_main_color", default_palette["old_main_color"])
	material.set_shader_parameter("old_shadow_color", default_palette["old_shadow_color"])
	material.set_shader_parameter("old_light_color", default_palette["old_light_color"])
	
	material.set_shader_parameter("new_main_color", color_palette["new_main_color"])
	material.set_shader_parameter("new_shadow_color", color_palette["new_shadow_color"])
	material.set_shader_parameter("new_light_color", color_palette["new_light_color"])

3. FEN and board setup

FEN (or Forsyth–Edwards Notation) is a way to encode the state of a chess board using letters for pieces (p or P for black and white Pawns, n or N for black and white Knights) and numbers for empty spaces.

Thus, you can encode for example the initial state of a chess board by writing

var start_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"

Then you just need to split by / and parse the substrings to setup the board. Easy!

func get_unit_info_from_symbol(symbol: String):
	var unit_type = null
	match symbol.to_lower():
		"p": unit_type = "pawn"
		"n": unit_type = "knight"
		"r": unit_type = "rook"
		"b": unit_type = "bishop"
		"q": unit_type = "queen"
		"k": unit_type = "king"
	var unit_side = "blue" if symbol.capitalize() == symbol else "red"
	var unit_info = {
		"unit_type": unit_type,
		"unit_side": unit_side
	}
	return unit_info

You can even use this notations to add a re-play system to your games.