I really enjoy creating Godot tools. The thought of making something that I can reuse over and over in future projects really excites me , and having others use my tools also makes me feel all warm and fuzzy inside. i was playing a few drops of Call of Duty Warzone on the weekend and decided to make a recreation of the Gulag map. The first tool I made for this was a modular “jail bars” tool, to create long lengths of bars that are evenly spaced, so i wouldn’t need to place each and every bar. This tool is pretty simple, but very effective and time saving.

I begin with some @export variables which will be used for the bar measurements and dimensions, along with a bool setter to trigger the bar generation.

@tool
extends Node3D
@export var bar_length: float = 3.0 : set = set_bar_length
@export var bar_width: float = 2.0 : set = set_bar_width
@export var bar_spacing: float = 0.2 : set = set_bar_spacing
@export var bar_thickness: float = 0.05 : set = set_bar_thickness
@export var bar_material: Material : set = set_bar_material
@export var generate_bars: bool = false : set = trigger_generate
@export var include_top_bar: bool = true
@export var include_lower_bar: bool = true

Next we setup all the functions that trigger each time one of the above exports values is changed and when the node first enters the scene tree and is ready.

func _enter_tree():
	generate_jail_bars()

func _ready():
	generate_jail_bars()

func set_bar_length(value: float):
	bar_length = max(0.1, value)
	call_deferred("generate_jail_bars")

func set_bar_width(value: float):
	bar_width = max(0.1, value)
	call_deferred("generate_jail_bars")

func set_bar_spacing(value: float):
	bar_spacing = max(0.01, value)
	call_deferred("generate_jail_bars")

func set_bar_thickness(value: float):
	bar_thickness = max(0.001, value)
	call_deferred("generate_jail_bars")

func set_bar_material(value: Material):
	bar_material = value
	call_deferred("update_material")

func trigger_generate(value: bool):
	if value:
		generate_jail_bars()

You probably notice the function “generate_jail_bars()” getting called in most of the above functions. This function along with “create_bar()” is really the meat and potatoes of this whole script. They do the work of spawning and arranging the bars within the earlier set @export variable values. They are being called within “call_deferred()” which essentially delays the function from being run until the end of the current frame – ( maybe call_deferred() require its own blog post one day! ). Lets write out those 2 main functions below.

func generate_jail_bars():
	if bars_container:
		bars_container.queue_free()
	bars_container = Node3D.new()
	bars_container.name = "BarsContainer"
	add_child(bars_container)
	var effective_width = bar_width - bar_spacing
	bar_count = max(1, int(effective_width / (bar_thickness + bar_spacing)))
	for i in range(bar_count):
		create_bar(i)
	
	# Create top and bottom horizontal bars is needed
	if include_top_bar:
		create_horizontal_bar(Vector3(0, bar_length / 2, 0))
	if include_lower_bar:
		create_horizontal_bar(Vector3(0, -bar_length / 2, 0))

func create_bar(index: int):
	var bar = MeshInstance3D.new()
	bar.name = "Bar_" + str(index)
	var mesh = CylinderMesh.new()
	mesh.height = bar_length
	mesh.top_radius = bar_thickness / 2
	mesh.bottom_radius = bar_thickness / 2
	mesh.radial_segments = 8
	bar.mesh = mesh
	var x_pos = (index - (bar_count - 1) / 2.0) * (bar_thickness + bar_spacing)
	bar.position = Vector3(x_pos, 0, 0)
	
	# Apply material if available
	if bar_material:
		bar.material_override = bar_material
	bars_container.add_child(bar)

The special sauce that positions all the bars neatly in a row is within the variable ‘x_pos’. Lets break down this calculation to see exactly what’s happening.

var x_pos = (index - (bar_count - 1) / 2.0) * (bar_thickness + bar_spacing)

(bar_count – 1) / 2.0 – This finds the center point of all the bars

index – (bar_count – 1) / 2.0 – This calculates how far each bar is from the center point

* (bar_thickness + bar_spacing) – This is calculating the bar and the spacing for its positioning.

bar.position = Vector3(x_pos, 0, 0)

Finally, the above code snippet sets the bar’s 3D position with the calculated x-coordinate, and y=0, z=0

I decided to add a top vertical and bottom vertical bar, as an option. This is so the bars don’t look like they are just floating in space, which is much more realistic and so this tool can be used without needing an upper and lower platform to “insert” to bars into.

func create_horizontal_bar(pos: Vector3):
	var bar = MeshInstance3D.new()
	bar.name = "HorizontalBar_" + str(pos.y)
	var mesh = BoxMesh.new()
	mesh.size = Vector3(bar_width, bar_thickness, bar_thickness)
	bar.mesh = mesh
	bar.position = pos
	
	# Apply material if available
	if bar_material:
		bar.material_override = bar_material
	bars_container.add_child(bar)

And finally a function to add the optional textures to the bars.

func update_material():
	if not bars_container:
		return
		
	for child in bars_container.get_children():
		if child is MeshInstance3D:
			child.material_override = bar_material

If you have read this far, feel free to use and modify this code to help create your own tool!

Leave a comment